SharePoint 2010 : Bonnes pratiques de gestion des logs et des exceptions

Je vous rassure, je ne vais pas vous refaire le couplet bien connu “Les exceptions c’est mal, il faut faire des TryCatch, etc…”, je pars du point de vue que vous le savez déjà et que vous essayez tant bien que mal de faire au mieux.

Ce ne sera une nouveauté pour personne aucun SharePointeur, vous avez la possibilité d’enregistrer le comportement de votre application dans les logs SharePoint (14/Logs) ou dans l’Event Viewer. Mais souvent, on se demande comment le faire, comment choisir entre l’un ou l’autre, etc. Au final, peu de développeurs respectent ces pratiques et on se retrouve souvent avec des développements bancals.

Voyons donc une manière simple et rapide de mettre en place la gestion des logs et exception dans SharePoint via un cas concret :

Je souhaite développer une webpart qui affiche tout les listes présentes sur le site courant. Et je souhaite que cette webpart dans les cas où elle viendrait à “planter”, affiche un message d’erreur “propre” aux utilisateurs et des informations détaillés aux Développeurs/Administrateurs. Ces informations seront disponibles sur les logs ou/et dans l’event viewer.

On va créer cette webpart, j’ai pour habitude de partir sur un développement basé sur le pattern MVP mais ce que je vous propose marchera aussi si vous avez envie de la faire à l’ancienne (ie. ASP.Net spaghetti).

On part sur une vue et un modèle très simple

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace DemoExceptionHandlingWebPart.ExceptionView
{
    interface IExceptionView
    {
        List<string> SetSiteData { set; }
    }
}
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.SharePoint;
 
namespace DemoExceptionHandlingWebPart.ExceptionView
{
    interface IExceptionViewModel
    {
        List<string> GetSiteData();
    }
}

La webpart en elle-même est très simple aussi. la seule particularité est l’utilisation d’un “ErrorVisualizer” dont je vous parlerais tout à l’heure.

using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;
using DemoExceptionHandlingWebPart.ExceptionHandling;
using System.Collections.Generic;
 
namespace DemoExceptionHandlingWebPart.ExceptionView
{
    [ToolboxItemAttribute(false)]
    public class ExceptionView : WebPart, IExceptionView
    {
        private ExceptionViewPresenter presenter;
        private GridView gvList = new GridView();
 
        public ExceptionView()
        {
        }
 
        protected override void CreateChildControls()
        {
            base.CreateChildControls();
 
            IErrorVisualizer errorVisualizer = new ErrorVisualizer(this);
            presenter = new ExceptionViewPresenter(this, new ExceptionViewModel());
            this.presenter.ErrorVisualizer = errorVisualizer;
            presenter.SetSiteData();
 
            Controls.Add(gvList);
        }
 
        public List<string> SetSiteData
        {
            set
            {
                gvList.ID = "gridViewList" + this.ID;
                gvList.DataSource = value;
                gvList.DataBind();
            }
        }
    }
}

On a donc ici, un minimum d’intelligence dans la vue, on fait appel au présenter et on instancie les contrôles dont on a besoin.

Vous vous en doutez, c’est dans le presenter que tout (ou presque) se passe.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using DemoExceptionHandlingWebPart.ExceptionHandling;
using Microsoft.SharePoint;
 
namespace DemoExceptionHandlingWebPart.ExceptionView
{
    class ExceptionViewPresenter
    {
        private IExceptionView view;
        private IExceptionViewModel model;
        private Random rand;
 
        public ExceptionViewPresenter(IExceptionView view, IExceptionViewModel model)
        {
            this.view = view;
            this.model = model;
            this.rand = new Random(DateTime.Now.Millisecond);
        }
 
        public IErrorVisualizer ErrorVisualizer { get; set; }
 
        public void SetSiteData()
        {
            int eventId = rand.Next(1000);
            try
            {
                if (eventId > 500)
                {
                    throw new SPException("The current user does not have the permissions to access this content");
                }
                else
                {
                    this.view.SetSiteData = this.model.GetSiteData();
                }
            }
            catch (Exception ex)
            {
                // If an unhandled exception occurs in the view, then instruct the ErrorVisualizer to replace
                // the view with an errormessage. 
                ViewExceptionHandler viewExceptionHandler = new ViewExceptionHandler();
                viewExceptionHandler.HandleViewException(ex, this.ErrorVisualizer, eventId);
            }
        }
 
    }
}

Comme je souhaite pouvoir tester mon application (ie. je veux la voir “crasher” souvent), j’ai décidé de faire une simple condition sur la valeur d’un rand.next. Si cette valeur est > 500 alors je lance une exception sinon je récupère les données de mon model. Le tout englobé dans un TryCatch dans le but de récupérer l’erreur et de la traiter dans le ViewExceptionHandler.

Voyons maintenant le reste de la webpart :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.SharePoint;
 
namespace DemoExceptionHandlingWebPart.ExceptionView
{
    class ExceptionViewModel : IExceptionViewModel
    {
        public List<string> GetSiteData()
        {
            SPWeb web = SPContext.Current.Web;
            List<string> result = new List<string>(web.Lists.Count);
 
            foreach (SPList list in web.Lists)
            {
                if (!list.Hidden)
                    result.Add(list.Title);
            }
 
            return result;
        }
    }
}

Et j’ai terminé mon travail.

Comment ça terminé mon travail ? Et la gestion des exceptions me direz vous ? Et bien tout est géré dans le ViewExceptionHandler et sa méthode HandleViewException

        /// <summary>
        /// Handle an exception in a view. This method will log the error using the ILogger that's registered in 
        /// the <see cref="SharePointServiceLocator"/> and will show the error in the <paramref name="errorVisualizer"/>
        /// </summary>
        /// <param name="exception">The exception to handle.</param>
        /// <param name="errorVisualizer">The error visualizer that will show the errormessage.</param>
        /// <param name="eventId">The EventId to log the error under.</param>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Visualizer"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "In this case, we are building an unhandled exception handler. The ThrowExceptionHandlingException will throw the exception for us")]
        [SharePointPermission(SecurityAction.InheritanceDemand, ObjectModel = true)]
        [SharePointPermission(SecurityAction.LinkDemand, ObjectModel = true)]
        public virtual void HandleViewException(Exception exception, IErrorVisualizer errorVisualizer, int eventId)
        {
            try
            {
                ILogger logger = GetLogger(exception);
                logger.LogToOperations(exception, eventId, EventSeverity.Error, null);
 
                EnsureErrorVisualizer(errorVisualizer, exception);
                errorVisualizer.ShowDefaultErrorMessage();
            }
            catch (ExceptionHandlingException)
            {
                throw;
            }
            catch (Exception handlingException)
            {
                this.ThrowExceptionHandlingException(handlingException, exception);
            }
        }

Bonne nouvelle c’est ici que votre travail s’arrête, tout le reste (comprenez toute la plomberie nécessaire à ce que le code ci-dessus fonctionne a déjà été codée par l’équipe de Patterns & Practices qui ont fait tout le travail pour vous (et moi).

En effet, ils ont développé une manière uniforme et très pratique de gérer une bonne fois pour toutes les erreurs dans SharePoint.

L’exemple que vous voyez ci-dessus est appliqué à une utilisation dans des Webpart avec un contrôle “ErrorVisualiser” qui va nettoyer la vue et n’afficher que le message d’erreur “propre” destiné à l’utilisateur (ici ShowDefaultErrorMessage). Le logToOperations s’occupera des informations techniques et les placera dans les logs ou dans l’event viewer en fonction de l’EventSeverity.

Si par contre vous souhaitez utiliser le travail de P&P dans un event receiver ou autre, vous pourriez faire :

// ... 
try 
{ 
  // ... a SharePoint operation ... 
} 
catch (SPException ex) 
{ 
  //Get an instance of ILogger 
  ILogger logger = SharePointServiceLocator.Current.GetInstance<ILogger>();       
  //Define your exception properties 
  string msg = "An error occurred while trying to retrieve the customer list";
  string category = @"SalesTool/Data"; 
  int eventID = 0; 
  EventLogEntryType severity = EventLogEntryType.Error; 
  //Log the exception 
  logger.LogToOperations(ex, msg, eventID, severity, category); 
}

Vous pourrez retrouver toute cette gestion d’erreur dans le dernier drop à cet url : http://codeplex.com/spg

PS : Je vous parlerai de ce SharePointServiceLocator dans les posts à venir.

<Philippe/>

Techdays 2010 : Slides sur SharePoint 2010 et TFS

Tout d’abord un grand merci d’être venu si nombreux à cette session. Cela fait plaisir de voir que l'industrialisation des développements SharePoint est un sujet qui interesse beaucoup de gens.

En attendant les vidéos qui ne devraient pas tarder à être publiées sur le site des Techdays, voici le powerpoint de la session SHA104 - SharePoint 2010 et Team Foundation Server.

Deux liens importants de cette présentation :

Pour vous donner l’esprit de cette présentation si vous n’avez pas eu la possibilité d’y assister : L’industrialisation des Développement SharePoint 2010 avec VSTS 2010 est vraiment simplifié par rapport à 2007 et je pense que nous allons voir de plus en plus d’équipe de développement qui vont mettre en place les pratiques que j’ai présenté dans ce ppt.

<Philippe/>

SharePoint 2010 : De nouvelles certifications en préparation

Selon le site des Partenaires Microsoft, de nouvelles certifications Sharepoint se préparent à débarquer pour Juin 2010 (cf https://partner.microsoft.com/global/40121316)

2 pour les Admin en version MCTS – Microsoft Certified Technology Specialist et MCITP – Microsoft Certified IT Professional

  • 70-667 TS: Microsoft SharePoint 2010, Configuring
    Microsoft Official Curriculum: Will cover configuration of SharePoint 2010 including deployment, upgrade, management, and operation on a server farm.
  • 70-668 PRO: SharePoint 2010, Administrator
    Microsoft Official Curriculum: Will cover advanced SharePoint 2010 topics including capacity planning, topology designing, and performance tuning.

2 pour Dév en version MCTS  et MCPD– Microsoft Certified Professional Developer

  • 70-573 TS: Microsoft SharePoint 2010, Application Development
    Microsoft Official Curriculum: Five-day instructor-led course designed for developers with six months or more of .NET development experience. Course covers what you need to know to be an effective member of a SharePoint development team using Visual Studio 2010.
  • 70-576 PRO: Designing and Developing Microsoft SharePoint 2010 Applications
    Microsoft Official Curriculum: Five-day instructor-led training course designed for development team leads who have already passed the Developing on SharePoint 2010 technical specialist exam. The course covers choosing technologies for and scoping a SharePoint project, best practices for SharePoint development, configuring a SharePoint development environment, advanced use of SharePoint developer features, and debugging of code in a SharePoint project.

Bien évidemment, la suprise n’est pas dans le passage de ces certifications en 2010 mais plutôt l’apparition du cursus MCPD et MCITP dans la branche SharePoint.

Personnellement, je vois d’un très bon oeil l’arrivée de certifications “PRO” qui manquaient cruellement aux certifications SharePoint 2007.

Vivement les béta :)

<Phil/>

SharePoint 2010 : Workshop à Lille le 14 Janvier

Histoire de revenir en forme pour la début d’année, je voulais vous faire part d’une invitation pour participer à un workshop sur lequel je travaille depuis quelque temps avec Frédéric Wickert de Microsoft :

Cette année nous allons accueillir Sharepoint 2010 et son lot de nouvelles fonctionnalités aussi intéressantes les unes que les autres.

clip_image001[4]

Avant le lancement officiel, je vous propose en avant-première dans la région de Lille, un atelier d’une journée sur Sharepoint 2010 animé par Frédéric Wickert et Philippe Sentenac.

Au menu :

- ½ journée sur les nouveautés fonctionnelles de Sharepoint 2010 : Vous pensez avoir tout vu ?…moi aussi….mais là…pfffff !!!!

- ½ journée sur les nouveautés pour les développeur Sharepoint avec Visual Studio 2010 : Etes-vous prêt à changer de vie ?….de développeur bien sûr !!

Cette présentation aura lieu dans les locaux de Microsoft Lille, le 14 janvier 2010 de 9h00 à 17h00.

N’hésitez pas à relayer cette invitation dans vos équipes et surtout venez nombreux !!

C’est un évènement réservé aux partenaires Microsoft. Merci de confirmer votre présence tout simplement par retour de mail à frederic.wickert@microsoft.com.”

PS : J’en profite pour souhaiter à tout le monde, une bonne et heureuse année !

SharePoint 2010 : Réunion du club utilisateur MOSS le 27 novembre

Je tenais à faire un post rapide pour vous rappeler que la prochaine réunion du club MOSS aura lieu le 27 Novembre.

A l'occasion de cette réunion, vous aurez la possibilité de retrouver énormément d'informations sur SharePoint 2010 avec des feedbacks de Microsoft et des MVP qui sont à la SharePoint Conférence.

La réunion a lieu à l'adresse habituelle : 39, quai du Président Roosevelt Issy-les-Moulineaux

Vous trouverez plus d'informations et des mises à jours à cette URL : http://clubmoss2007.org/ReunionNOVEMBRE2009.aspx

--

Je profite d'ailleurs de ce post pour marquer mon retour sur la blogosphère SharePoint après 3 mois de "repos" bien occupés (mariage, bébé en route et changement de boite).

clip_image001

En effet, après pratiquement 3 ans chez Winwise, j'ai décidé de rejoindre Wygwam, la "fameuse société lilloise" et ses projets de développement en région Parisienne.

Nul doute qu'entre Christian, Renaud et moi vous allez beaucoup entendre parler de Wygwam et de SharePoint cette année :)

<Phil/>

SharePoint 2007 : Création d’un Quizz dans SharePoint, Récapitulatif

SharePoint 2007 : patterns & practices SharePoint Guidance

je vous avais déjà parlé de ce projet Codeplex mené par l’équipe de P&P sur un guide des bonnes pratiques de développement dans des posts précédents.

Nous en sommes au 12eme drop (Un drop par semaine) pour la version V2 et la documentation commence à être de plus en plus étoffée.

Je suis personnellement fan des points sur le SharePoint Logger, Application Configuration, et surtout le ListRepository en combinaison avec le service Locator. Je vous montrerais sans doute une implémentation en utilisant un exemple avec le Quizz (cf précédent posts)

image

Jetez y un coup d’oeil avant les vacances, ca vaut le déplacement.

<Philippe/>

SharePoint 2007 : Un peu de qualité dans ce monde de brutes

Afin de me remettre un peu en mode blogueur (cela faisait un petit moment que j’étais loin de ce blog), je me suis dis qu’il serait bien de clarifier un peu mes propos par rapport à mes derniers posts.

En effet, depuis les posts : SharePoint 2007 : Utilisation du pattern MVP et Tests Unitaires avec TypeMock et SharePoint 2007 : Accès aux données et Test Unitaires, j’ai eu de nombreux retours via collègues et blog me demandant de clarifier deux points importants :

  1. Quel est l’apport de TypeMock dans la réalisation de test unitaires appliqué à SharePoint ?
  2. Quel intérêt de faire des applications utilisant les patterns MVP, Test Unitaires, Test Web, voire de l’industrialisation des développements SharePoint ?

Concernant le premier point, sur l’intérêt de TypeMock, il faut toute d'abord se rendre compte que toute application SharePoint a une facheuse tendance à avoir une dépendance sur l’environnement SharePoint (J’enfonce des portes ouvertes, j’en ai bien conscience mais suivez ma logique). En effet, quand je développe une webpart, mon accès aux donnée va exécuter des requêtes CAML sur un SPWeb, un SPSite, etc..

Dans nos développements, Ce lien à SharePoint à 3 inconvénients majeurs :

  • Nécessité d’avoir un environnement SharePoint lors de l’exécution des test (unitaires/web/charges)
  • Un ralentissement dans l’exécution des tests pendant la phase d’accès aux données
  • Nous souhaitons tester uniquement notre code et pas telle ou telle méthode de mon objet SharePoint déjà testé par Microsoft.

Idéalement, nous voudrions donc remplacer l’appel au modèle objet SharePoint par des “Fake”. Un objet “Fake” est un objet créé par le développeur dans le cadre d’un test unitaire qui va permettre de passer les paramètres souhaités dans le contexte du test donné tout en conservant le type de l’objet initial.

En clair, appliqué à SharePoint, si j’ai une webpart qui doit afficher le Titre du SPWeb courant dans un champ texte, j’accéderai par exemple à SPContext.Current.Web.Title. Dans ce cas, je voudrais avoir plusieurs tests unitaires qui vont vérifier les cas d’erreur (Titre vide, longueur de chaine excessivement longue, etc…).

Je voudrais donc avoir la possibilité de paramétrer mes tests unitaires en passant à la webpart quelque chose dans ce style :

   1:  SPWeb webWithTitleNullOrEmpty = new SPWeb(){ Title = String.Empty };
   2:  SPWeb webWithTitleLong = new SPWeb(){ Title = “TrèsLongueChaineEtc…” };
   3:   
   4:  // Test chacun des cas ....

Mon problème est que la manière d’obtenir un SPWeb diffère pour chacun de ces tests et m'oblige donc à avoir un site web différent pour chaque cas sur mon environnement :

   1:  using (SPSite site = new SPSite(“http://dev-sharepoint/collectiondesite”))
   2:  {
   3:       SPweb webWithTitleNullOrEmpty = site.OpenWeb(“WebWithTitleNull”);
   4:       // etc..
   5:  }

En effet, je n’ai pas de constructeur qui me permettrait d’initialiser ce genre d’objet dans le modèle objet SharePoint selon mes besoins, je suis obligé de me basé sur un environnement SharePoint réel. Je ne vous parle pas du temps nécessaire pour créer tout les variantes pour les test unitaires sur vos environnements, ce n’est tout simplement pas possible de fonctionner comme ça.

Ceux qui connaissent des Frameworks comme RhinoMock ou Mock, auront tendance à vouloir utiliser ces frameworks pour créer un “Fake” des objets SharePoint mais malheureusement, ces frameworks sont incapables de générer des “Fake” à partir de classes qui sont “Sealed” ou qui n’ont pas de constructeur public.

TypeMock

Hors l’API SharePoint utilise de façon intensive des classes “Sealed” et des classes avec des constructeurs “Internal” et à l’heure actuelle, TypeMock est le seul Framework de Mock qui permet de générer ces “Fake”.

   1:  // Création d'un fake SPContext
   2:   
   3:  SPContext context = Isolate.Fake.Instance<SPContext>(Members.ReturnRecursiveFakes);
   4:   
   5:  // Possibilité de paramètrer le fake pour qu'il retourne telle ou telle valeur...
   6:   
   7:  Isolate.WhenCalled(() => context.Current.Web.Title).WillReturn("Hello World");

En résumé : Si vous souhaitez faire des tests unitaires appliqués à SharePoint et que vous souhaitez les exécuter rapidement et sans dépendance sur un environnement SharePoint, la seule solution à l’heure actuelle est TypeMock.

Concernant le deuxième point, sur l’intérêt des patterns MVP, Test Unitaires, Test Web, bref de la série de posts que j’ai faite depuis quelques mois…, mon point de vue est sans doute biaisé :)

Depuis plus de 2 ans de SharePoint, j’ai appris que réussir des projet SharePoint n’est pas forcément facile surtout quand il y a un pourcentage non négligeable de développement personnalisé dans le cadre du projet.

Ceci s’explique par de nombreuses raisons :

SharePoint nécessite de travailler dans un environnement virtualisé. La création de cet environnement prend du temps, en moyenne une journée par développeur. Souvent, les responsables de projets ne pensent pas à planifier ce temps là et dès le départ du projet on se retrouve rapidement avec une semaine de retard pour une petite équipe de 5 développeurs. Heureusement, il existe plusieurs moyens pour réduire ce délai (cf SharePoint 2007 : Automatiser la création de votre environnement SharePoint)

Les développeurs/architectes doivent non seulement assimiler deux Frameworks supplémentaires (WSS et MOSS) en plus du .NET et d’ASP.Net mais aussi connaîttrent parfaitement la partie administration afin de pouvoir différencier une demande de fonctionnalité Standard d’une demande de développement. Cette connaissance ne s’acquiert pas en un jour et encore moins sans accompagnement ou formation… Ne pas s’assurer que l’équipe est correctement formée a bien souvent mené de nombreux projets à la poubelle. C’est valable pour toutes les technologies ceci-dit,mais la particularité de SharePoint est qu’il demande plusieurs paires de bras pour bien assimiler le produit.

coach sharepoint

Selon mon expérience, une personne accompagnée ou formée sur WSS/MOSS est la plupart du temps 3 à 4 fois plus rapide qu’un débutant. Imaginez les impacts sur vos plannings de projet… C’est d’ailleurs pour cette raison que Microsoft et plusieurs autres entités (Combined Knowledge notamment) n’ont eu de cesse de crée du contenu de formation dans le but de faciliter la montée en compétences de vos équipes (cf SharePoint 2007 : Découvrir et se former à SharePoint)

Mais cette connaissance de SharePoint à d’autres implications. Il y a quelques spécificités SharePoint comme la génération de packages WSP, le déploiement, les bonnes pratiques concernant la libération de la mémoire (cf SharePoint 2007 : Dispose Patterns et l'outil SPDisposeCheck) qui misent bout à bout peuvent grandement influencer la bonne santé de votre projet. De plus, la plupart des guides de développement SharePoint restent assez basiques et n’utilisent pas ou très peu d’architectures connues et reconnues (3 couches, Patterns, etc…) et se reposent souvent sur ce qu’on appelle l’ASP.Net Spaghetti.

Heureusement depuis peu, une lumière apparaît au bout du tunnel, un exemple de bonnes pratiques de développement nous ait montré  par l’équipe de Patterns & Practices (cf SharePoint 2007 : Patterns & Practices Guidance). Leur exemples de développement SharePoint nous montre qu’il est aussi possible dans SharePoint de “coder proprement” et même d’avoir des tests unitaires/web/charges.

patterns and practices

Néanmoins, soyons honnêtes, autant le fait de dire “Coder proprement, c’est mieux” suffira la plupart du temps à convaincre un développeur de la validité d’une discussion sur tel ou tel bout de code autant un fonctionnel ou un décideur n’y verra pas ou peu d’intérêt. “Tant que ça marche, quel différence peut il y avoir”…

WTF Minutes

A mes yeux, baser les développements SharePoint sur l’utilisation de divers patterns ou tout simplement “bien coder” (cf : SharePoint 2007 : Utilisation du pattern MVP et Tests Unitaires avec TypeMock) va permettre une meilleure isolation entre chaque couches de vos développements. Cette isolation va potentiellement rendre le code plus lisible et entre autres choses permettre l’utilisation de test unitaires/charges/web. Un code plus lisible et mieux testé est souvent certes plus long à créé mais sur le long terme coutera moins d’argent et temps :

  • Le code est plus simple à lire pour les nouveaux arrivant dans l’équipe =  Moins de temps de perdu tout au long du projet lors des changements dans l’équipe.
  • Le code est testé ce qui va réduire drastiquement le nombre de bugs = Moins de temps de perdu pendant la phase de développement, pendant la phase de recette et pendant la période de support à la fois pour le client et pour l’équipe de développement.
  • L’application est plus facile à mettre à jour (il est plus facile de remplacer tout ou partie de l’application) = Moins de temps de perdu pendant la phase de développement et pendant la période de support à la fois pour le client et pour l’équipe de développement.

Pour finir, l’idée même de cette série sur l’industrialisation des développement SharePoint (avec Team System) est de montrer qu’il est possible d’avoir pour vos projets SharePoint :

  • un Gain de temps avant, pendant et après le projet
  • une Aide à la gestion et au suivi de projet
  • une Amélioration de la qualité
  • et une Réduction des coûts

Industrialisation des Développements SharePoint

Voilà quelques liens pour vous donner quelques idées ce qu’il est possible de faire :

J’espère que c’est un peu plus clair maintenant :)

<Philippe/>

SharePoint 2010 : Annonce officielle des pré-requis d’installation et détails sur le support et la compatibilité des navigateurs

[via Microsoft SharePoint Team Blog ]

Ca y est, c’est officiel.

  1. SharePoint Server 2010 sera uniquement en version 64-bit (cela avait déjà été annoncé précédemment).
  2. SharePoint Server 2010 nécessitera Windows Server 2008 64-bit ou Windows Server 2008 R2 64-bit.
  3. SharePoint Server 2010 nécessitera SQL Server 2008 64-bit  ou SQL Server 2005 64-bit.

De plus, afin d’assurer la meilleure expérience à travers le plus de navigateurs possible, l’équipe de développement de SharePoint 2010 se concentre sur les navigateurs suivants sur la plateforme Windows :

A savoir :

  • Internet Explorer 7
  • Internet Explorer 8,
  • Firefox 3.x.

A noter que la compatibilité de Firefox 3.X et Safari 3.X sous les systèmes Non-Windows sera améliorée.

Par contre, Internet Explorer 6 ne sera pas supporté pour SharePoint 2010. En effet,le support pour IE 6 se termine en Juillet 2010. Plus d’infos sur http://support.microsoft.com/gp/lifepolicy.

Pour finir, pour ceux qui s’inquiètent des conséquences pour la visibilité des sites Internet SharePoint 2010 à cause de cet arrêt du support d’IE 6, sachez que SharePoint 2010 fournira au développeur un contrôle total sur l’HTML généré ainsi que sur les styles de ces pages. Cette fonctionnalité permettra au client d’avoir des sites compatibles avec un plus grand panel de navigateurs pour l’affichage du contenu (ce qui inclue IE 6). Néanmoins, Internet Explorer 7, Internet Explorer 8 or Firefox 3.x resteront nécessaire pour la création de contenu web.

Bien sur, le but de cette annonce est de vous permettre de planifier et de budgéter au mieux la mise en place de cette nouvelle plateforme.

Allez plus que quelques mois à attendre…

<Philippe/>

SharePoint 2007 : Utilisation du pattern MVP et Tests Unitaires avec TypeMock

Comme nous avons pu le voir dans le dernier post à ce sujet, après avoir fait l’accès aux données pour la fonctionnalité Quizz, il est maintenant nécessaire de développer “l’intelligence” de la webpart.

Dans notre cas, cela se résume à 3 fonctionnalités :

  1. l’initialisation de la webpart
  2. La validation des données saisies par l’utilisateur
  3. Le passage à la prochaine question

Mais avant de commencer, il est nécessaire de comprendre comment cette webpart est “architecturée”. En effet, dans le but de faciliter au maximum l’utilisation de test unitaires et le découplage de l’application, j’ai choisi d’utiliser le pattern MVP (Model, View, Présenter) qui est en quelque sorte l’ancêtre du pattern MVC (Model , View, Controller).

Évidemment, ce travail est fortement basé sur le travail réalisé en amont par l’équipe de Patterns and Practices sur SharePoint que vous pourrez retrouvez sur codeplex : http://spg.codeplex.com ou sur le blog de Erwin van der Valk

image

Vous verrez qu’il existe déjà beaucoup d’informations liées à ce pattern sur la toile, je les ai regroupées sur mon compte delicious, que vous pourrez retrouver à cette url : http://delicious.com/philippesentenac/mvp

Pour faire simple, nous allons développé une webpart qui va charger la View (un user control), cette vue (qui n’est là que pour le design de l’application) va instancier son présenter qui s’occupera de l’accès au données et de tout les traitement nécessaire (validation, erreur, etc.) et qui communiquera avec la vue via une interface.

Mais assez de discours, voyons tout cela dans le détail :

   1:  using TechDays.Demo.SharePoint.BusinessEntities;
   2:   
   3:  namespace TechDays.Demo.SharePoint.Presenter
   4:  {
   5:      /// <summary>
   6:      /// Interface for the Quizz WebPart
   7:      /// </summary>
   8:      public interface IQuizzView
   9:      {
  10:          /// <summary>
  11:          /// Gets or sets the name of the list.
  12:          /// </summary>
  13:          /// <value>The name of the list.</value>
  14:          string ListName { get; set; }
  15:   
  16:          /// <summary>
  17:          /// Gets or sets the question.
  18:          /// </summary>
  19:          /// <value>The question.</value>
  20:          Question Question { get; set; }
  21:   
  22:          /// <summary>
  23:          /// Gets or sets the error message.
  24:          /// </summary>
  25:          /// <value>The error message.</value>
  26:          string ErrorMessage { get; set; }
  27:      }
  28:  }

Ici, nous avons donc une interface très simple qui nous permettra de communiquer avec le presenter notamment concernant la liste sur laquelle l’utilisateur souhaite effectué le quizz, la question actuellement affichée par la webpart et eventuellement un message d’erreur.

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Text;
   4:  using SharePointOfView.Patterns.MVP;
   5:  using SharePointOfView.Utilities;
   6:  using TechDays.Demo.SharePoint.BusinessEntities;
   7:  using TechDays.Demo.SharePoint.Model;
   8:   
   9:  namespace TechDays.Demo.SharePoint.Presenter
  10:  {
  11:      /// <summary>
  12:      /// The Presenter for the Quizz WebPart
  13:      /// </summary>
  14:      [CLSCompliant(false)]
  15:      public class QuizzPresenter : Presenter<IQuizzView>
  16:      {
  17:          /// <summary>
  18:          /// Called when the view is initialized.
  19:          /// </summary>
  20:          public override void OnViewInitialized()
  21:          {
  22:              if (View.Question == null)
  23:              {
  24:                  try
  25:                  {
  26:                      var quizzRep = new QuestionRepository(this.SPContext.Web, this.View.ListName);
  27:                      Question question = quizzRep.GetNextQuestion(0);
  28:                      View.Question = question;
  29:                  }
  30:                  catch (Exception ex)
  31:                  {
  32:                      string errorMessage = Localization.GetResource("Quizz_Presenter_ErrorMessage", "Quizz.Presenter", this.SPContext.Web.Language);
  33:                      View.ErrorMessage = errorMessage + ex.Message;
  34:                  }
  35:              }
  36:          }
  37:   
  38:          /// <summary>
  39:          /// Validates the specified answers.
  40:          /// </summary>
  41:          /// <param name="answers">The answers.</param>
  42:          /// <param name="explanation">The explanation.</param>
  43:          /// <returns>True is the answers are correct, else False</returns>
  44:          public bool Validate(Dictionary<int, bool> answers, out string explanation)
  45:          {
  46:              StringBuilder explanationBuilder = new StringBuilder();
  47:              explanation = string.Empty;
  48:   
  49:              if (View.Question == null)
  50:              {
  51:                  View.ErrorMessage = Localization.GetResource("Quizz_Presenter_ErrorMessage", "Quizz.Presenter", this.SPContext.Web.Language);
  52:                 return false;
  53:              }
  54:   
  55:              bool correct = true;
  56:              Question currentQuestion = View.Question;
  57:   
  58:              foreach (KeyValuePair<int, bool> answer in answers)
  59:              {
  60:                  if (currentQuestion.Answers[answer.Key] == null || currentQuestion.Answers[answer.Key].IsValid != answer.Value)
  61:                  {
  62:                      correct = false;
  63:                  }
  64:              }
  65:   
  66:              string labelCorrect = Localization.GetResource("Quizz_Presenter_Correct", "Quizz.Presenter", this.SPContext.Web.Language);
  67:              string labelIncorrect = Localization.GetResource("Quizz_Presenter_Incorrect", "Quizz.Presenter", this.SPContext.Web.Language);
  68:              string formattedExplanation = Localization.GetResource("Quizz_Presenter_ExplanationFormat", "Quizz.Presenter", this.SPContext.Web.Language);
  69:   
  70:              explanationBuilder.AppendFormat(formattedExplanation, correct ? labelCorrect : labelIncorrect);
  71:              currentQuestion.Answers.FindAll(answ => answ.IsValid == true).ForEach(delegate(Answer answ) { explanationBuilder.Append(answ.Content + " "); });
  72:              explanation = explanationBuilder.ToString().TrimEnd();
  73:   
  74:              return correct;
  75:          }
  76:   
  77:          /// <summary>
  78:          /// Sets the next question.
  79:          /// </summary>
  80:          /// <param name="questionId">The question id.</param>
  81:          public void SetNextQuestion(int questionId)
  82:          {
  83:              var quizzRep = new QuestionRepository(this.SPContext.Web, this.View.ListName);
  84:              Question nextQuestion = quizzRep.GetNextQuestion(questionId);
  85:              View.Question = nextQuestion;
  86:          }
  87:      }
  88:  }
Ici on retrouve donc les 3 actions que le presenter doit traiter, nous allons maintenant les détailler en commentant un peu plus le code :
  1. l’initialisation de la webpart : Si la vue n’as pas déjà été initialisée, on instancie notre QuestionRepository (vu dans le précédent post), on récupère la première question de la liste et on assigne cette question à la vue. En cas d’erreur, on affiche un message adéquat.
  2. La validation des données saisies par l’utilisateur : On récupère la question provenant de la vue et la/les réponse(s) de l’utilisateur et on vérifie si ces réponses sont correctes. En fonction on retourne un booléen et un chaine de caractères informant l’utilisateur de son résultat.
  3. Le passage à la prochaine question : On instancie simplement notre repository et on assigne la vue à la question suivante.

Pour ceux qui se poseraient la question, voilà comment est définie une question :

   1:  using System;
   2:  using System.Collections.Generic;
   3:   
   4:  namespace TechDays.Demo.SharePoint.BusinessEntities
   5:  {
   6:      /// <summary>
   7:      /// Used to store all the informations about a Quizz Question
   8:      /// </summary>
   9:      [Serializable()]
  10:      public class Question
  11:      {
  12:          /// <summary>
  13:          /// Gets or sets the ID.
  14:          /// </summary>
  15:          /// <value>The Question's ID</value>
  16:          public int Id { get; set; }
  17:   
  18:          /// <summary>
  19:          /// Gets or sets the content.
  20:          /// </summary>
  21:          /// <value>The content.</value>
  22:          public string Content { get; set; }
  23:   
  24:          /// <summary>
  25:          /// Gets or sets the category.
  26:          /// </summary>
  27:          /// <value>The category.</value>
  28:          public Category Category { get; set; }
  29:   
  30:          /// <summary>
  31:          /// Gets or sets the answers.
  32:          /// </summary>
  33:          /// <value>The answers.</value>
  34:          public List<Answer> Answers { get; set; }
  35:      }
  36:   
  37:      /// <summary>
  38:      /// Used to know any choices for a question and if they are valid
  39:      /// </summary>
  40:      [Serializable()]
  41:      public class Answer
  42:      {
  43:          /// <summary>
  44:          /// Gets or sets the content.
  45:          /// </summary>
  46:          /// <value>The content.</value>
  47:          public string Content { get; set; }
  48:   
  49:          /// <summary>
  50:          /// Gets or sets a value indicating whether this instance is valid.
  51:          /// </summary>
  52:          /// <value><c>true</c> if this instance is valid; otherwise, <c>false</c>.</value>
  53:          public bool IsValid { get; set; }
  54:      }
  55:   
  56:      /// <summary>
  57:      /// Used to store the information about a Quizz Category
  58:      /// </summary>
  59:      [Serializable()]
  60:      public class Category
  61:      {
  62:          /// <summary>
  63:          /// Gets or sets the title.
  64:          /// </summary>
  65:          /// <value>The title.</value>
  66:          public string Title { get; set; }
  67:   
  68:          /// <summary>
  69:          /// Gets or sets the ID.
  70:          /// </summary>
  71:          /// <value>The Category's ID</value>
  72:          public int Id { get; set; }
  73:      }
  74:  }

Le plus dur est fait a priori, maintenant j’aimerai pouvoir m’assurer que tout celà fonctionne bien. Je vais donc coder quelques tests unitaires notamment avec l’utilisation de TypeMock.

Pour bien comprendre l’intérêt des tests qui vont suivre, il faut bien voir que dans le précédent post, j’ai valider que mon accès au données fonctionnait correctement. Dorénavant lorsque je ferais appel à mon QuestionRepository dans mes test unitaires, je pourrais “mocker” cet accès afin de rendre mes tests isolés de l’envionnement SharePoint.

   1:  using System.Collections.Generic;
   2:  using Microsoft.SharePoint;
   3:  using Microsoft.VisualStudio.TestTools.UnitTesting;
   4:  using TechDays.Demo.SharePoint.BusinessEntities;
   5:  using TechDays.Demo.SharePoint.Model;
   6:  using TechDays.Demo.SharePoint.Presenter;
   7:  using TypeMock.ArrangeActAssert;
   8:   
   9:  namespace TechDays.Demo.SharePoint.Presenter.Tests
  10:  {
  11:      /// <summary>
  12:      /// Test Class that will handle all the Tests for the Quizz Presenter
  13:      /// </summary>
  14:      [TestClass]
  15:      public class QuizzPresenterTests
  16:      {
  17:          #region Fields
  18:          ...
  19:          #endregion
  20:   
  21:          /// <summary>
  22:          /// Initializes a new instance of the QuizzPresenterTests class.
  23:          /// </summary>
  24:          public QuizzPresenterTests()
  25:          {
  26:              IQuizzView view = Isolate.Fake.Instance<IQuizzView>(Members.ReturnNulls);
  27:              SPContext context = Isolate.Fake.Instance<SPContext>(Members.ReturnRecursiveFakes);
  28:              this.presenter = new QuizzPresenter();
  29:              this.presenter.View = view;
  30:              this.presenter.SPContext = context;
  31:   
  32:              this.questionRepository = Isolate.Fake.Instance<QuestionRepository>();
  33:   
  34:              #region Init Questions q1 & q2
  35:              ...
  36:              #endregion
  37:   
  38:              this.questions = new List<Question> { q1, q2 };
  39:          }
  40:   
  41:          /// <summary>
  42:          /// When onViewInitialized is called, it should set the view at the first Quizz Animaux's question.
  43:          /// </summary>
  44:          [TestMethod]
  45:          [Isolated]
  46:          public void OnViewInitializedShouldSetViewAtFirstQuestionOfQuizzAnimaux()
  47:          {
  48:              Isolate.Swap.NextInstance<QuestionRepository>().With(this.questionRepository);
  49:              Isolate.WhenCalled(() => this.questionRepository.GetNextQuestion(0)).WillReturn(this.questions[0]);
  50:   
  51:              this.presenter.OnViewInitialized();
  52:   
  53:              Assert.IsNotNull(this.presenter.View.Question);
  54:              Assert.AreEqual(this.questions[0].Content, this.presenter.View.Question.Content);
  55:          }
  56:      }
  57:  }
Regardons ce qu’il se passe pendant le test de la première méthode : OnViewInitializedShouldSetViewAtFirstQuestionOfQuizzAnimaux.
  • ligne 48, Grâce à TypeMock, je précise que la prochaine fois que l’objet QuestionRepository est appelé dans la suite de mon code, j’utiliserai ma propre instance.
  • ligne 49, je définie explicitement que l’appel à la méthode GetNextQuestion(0) retournera la question q1.
  • ligne 50, j’appelle “normalement” la méthode
  • ligne 51, je vérifie que le traitement a bien été effectué.

L’avantage d’utiliser Typemock dans ce cas précis est de nous permettre de nous dissocier complètement de SharePoint et de son SPContext en créant des Fake. Notamment gràce à cette ligne de code  :

SPContext context = Isolate.Fake.Instance<SPContext>(Members.ReturnRecursiveFakes));

Ainsi, si on prend un méthode un peu plus “complexe” comme la validation. On peut réaliser ce genre de test :

   1:  /// <summary>
   2:  /// When Validate is called, it should return false and an "Incorrect" explanation.
   3:  /// </summary>
   4:  [TestMethod]
   5:  [Isolated]
   6:  public void ValidateShouldReturnFalseAndIncorrectExplanation()
   7:  {
   8:      // Arrange
   9:      Isolate.Swap.NextInstance<QuestionRepository>().With(this.questionRepository);
  10:      Isolate.WhenCalled(() => this.questionRepository.GetQuestion(1)).WillReturn(this.questions[0]);
  11:   
  12:      Dictionary<int, bool> userAnswers = new Dictionary<int, bool>();
  13:      userAnswers.Add(0, true);
  14:      userAnswers.Add(1, true);
  15:      userAnswers.Add(2, false);
  16:      string explanation = string.Empty;
  17:      this.presenter.View.Question = this.questions[0];
  18:   
  19:      // Act
  20:      bool expected = this.presenter.Validate(userAnswers, out explanation);
  21:   
  22:      // Assert
  23:      Assert.IsFalse(expected);
  24:      Assert.AreEqual("Incorrect ! la réponse est : Egypte", explanation);
  25:  }

Ici, je simule l’interaction utilisateur en générant moi-même les réponses (userAnswers), récupère la question q1 et Fake le contexte SharePoint.

Par la suite, je fais appel à la méthode et verifie que le résultat obtenu correspond au resultat attendu.

image

Au final, nous avons réussi à obtenir nos tests unitaires de notre WebPart dans notre environnement Visual Studio avec une minimum de dépendance à SharePoint.

La suite du pattern MVP au prochain post :)

PS : Vous pourrez retrouvez tout celà dans la présentation que nous avons donné au Techdays 2009 avec Adrien. [Microsoft TechDays 2009] - Industrialisation du développement SharePoint avec Team System - 1/3

<Philippe/>

SharePoint 2007 : Automatiser la création de votre environnement SharePoint

Un petit post rapide pour linker une excellente série faite par Emmanuel Bergerat sur son blog : Automating WSS v3/MOSS 2007 development environment setup

Nombreux sont ceux qui connaissent le temps que cela peut prendre de devoir créer ses VPC “à la main” et cette série sera très utile à tout ceux qui n’avaient pas encore “industrialisé” cette partie rébarbative dans leur phase de développement.

Ci-dessous la liste des étapes :

    1. Planning
    2. Decisions
    3. Windows Server 2003 unattended setup
    4. Unattended DC promo
    5. Active Directory preparation & SQL Server 2005 SP2 setup
    6. SharePoint prerequisites setup
    7. SharePoint binaries setup
    8. SharePoint configuration wizard
    9. Setup MOSS 2007 farm with minimum settings
    10. Setup Visual Studio 2008 SP1 + Team Foundation Explorer
    11. Setup Visual Studio extensions for WSS v1.3
    12. Setup Microsoft Office Enterprise 2007 Enterprise
    13. Setup Microsoft SharePoint Designer 2007
    14. Setup Internet Explorer 7 developer toolbar, WSS v3 SDK and MOSS 2007 SDK
    15. Add useful external tools
    16. Implement SPDevmod from Scot Hillier

Ironiquement, L’étape suivante semble avoir déjà été désigné : Macaw Solutions Factory

Sans entrer dans les détails, cela permet de créer un environnement de développement personnalisé et virtualisé à la demande grâce à un joyeux mix de Microsoft Virtual Machine Manager 2008, Hyper-V Server 2008, Microsoft Deployment Toolkit, etc..

image

4 OS sont disponibles : Windows 2003 x86 et x64, Windows 2008 x86 et x64.

Après que le nom de la machine et le mot de passe admin aient été renseigné la machine virtuelle est créée, déployée sur le serveur et démarrée.

Il est actuellement possible d’installer les composants suivants (tout ou partie) :

  • .NET Framework 3.5 Sp1
  • Visual Studio 2005, 2008
  • SQL Server 2005, 2008, avec Analysis, Integration, et Reporting Services
  • WSS3 et MOSS 2007
  • Biztalk 2006 R2
  • Firefox, 7Zip, Adobe Reader, Flash et Silverlight

image

Je connais un petit paquet d’IT qui seraient bien content d’avoir cette fonctionnalité à leur disposition.

Il me semble que le produit est/sera payant. Désolé pour le manque de clarté mais le site était partiellement en allemand :(

<Philippe/>

SharePoint 2007 : Découvrir et se former à SharePoint

On me demande souvent en clientèle si je connais quelques liens qui permettraient de s’auto-former à SharePoint aussi bien du coté Développeur que du coté Utilisateur.

Du coup, je me suis dit qu’un petit post qui récapitule tout les sources d’autoformation serait bien pratique pour tout le monde (moi y compris).

Pour Utilisateurs

Pour Développeurs / IT

De plus, Je viens de voir que le Coach SharePoint est disponible sur la MSDN à cette URL : http://msdn.microsoft.com/fr-fr/office/msdn.coach.sharepoint.aspx

L’objectif de ce coach est de vous guider dans la réalisation d’un modèle de site entièrement personnalisé.

Les différents ateliers sont :

Atelier 1 : Découvrir le système SharePoint

Atelier 2 : Créer votre propre modèle de site SharePoint : un Site Definition

Atelier 3 : Créer vos propres Features SharePoint

Atelier 4 : Créer votre Feature avec code behind pour SharePoint

Atelier 5 : Développez une WebPart simple (atelier à venir)

Atelier 6 : Packagez et déployez votre projet via le système des solutions (atelier à venir)

Un bon petit programme avec en plus de chouettes images (j’aime beaucoup celle de 'l’atelier 4).

 

imageimageimage  

Au final, ce coach nous donne une bonne source d’autoformation pour les développeurs Français.

Félicitations à ces auteurs.

<Philippe/>

SharePoint 2007 : Accès aux données et Test Unitaires

Pour reprendre l'exemple du Quizz là où je l'avais laissé il y a quelques semaines (cf le post de janvier ), il est désormais temps de se mettre à coder et à commencer par une des parties critiques de toutes applications : l'accès aux données.

Dans notre cas, l'accès au données se décompose en 3 parties : CategoryRepository, QuizzRepository, QuestionRepository ,

Prenon l'exemple de QuestionRepository est définit par l'interface suivante :

   1:      public interface IQuestionRepository
   2:      {
   3:          /// <summary>
   4:          /// Gets the next question.
   5:          /// </summary>
   6:          /// <param name="index">The index.</param>
   7:          /// <param name="category">The category.</param>
   8:          /// <returns>A Question</returns>
   9:          /// <exception cref="System.ArgumentException">Thrown when index is lower than 0</exception>
  10:          Question GetNextQuestion(int index, Category category);
  11:   
  12:          /// <summary>
  13:          /// Gets the next question.
  14:          /// </summary>
  15:          /// <param name="index">The index.</param>
  16:          /// <returns>A Question</returns>
  17:          /// <exception cref="System.ArgumentException">Throw when index is lower than 0</exception>
  18:          Question GetNextQuestion(int index);
  19:   
  20:          /// <summary>
  21:          /// Gets the question.
  22:          /// </summary>
  23:          /// <param name="index">The index.</param>
  24:          /// <returns>A Question</returns>
  25:          /// <exception cref="System.ArgumentException">Thrown when index is lower than 0</exception>
  26:          Question GetQuestion(int index);
  27:      }

A moi maintenant de développer l'accès aux données SharePoint en fonction de mes besoins (qui restent ici excessivement simples puisqu'il ne s'agit que d'un simple accès en lecture).

J'aurais pu décider d'implémenter l'accès au données via Linq4SP mais pour l'instant dans le cadre de ce post, je vais me restreindre à du bon vieux CAML et uniquement à l'implémentation de l'interface IQuestionRepository.

   1:  /// <summary>
   2:      /// This class is used to access the items inside a Quizz List at a specific Website
   3:      /// </summary>
   4:      [CLSCompliant(false)]
   5:      public class QuestionRepository : IQuestionRepository
   6:      {
   7:          #region Private Members
   8:          /// <summary>
   9:          /// The Quizz list where all the Queries will be processed
  10:          /// </summary>
  11:          private SPList quizzList;
  12:          #endregion
  13:   
  14:          #region Constructors
  15:   
  16:          /// <summary>
  17:          /// Initializes a new instance of the <see cref="QuestionRepository"/> class.
  18:          /// </summary>
  19:          /// <param name="web">The web used.</param>
  20:          /// <param name="listName">Name of the list.</param>
  21:          public QuestionRepository(SPWeb web, string listName)
  22:          {
  23:              if (web == null || string.IsNullOrEmpty(listName))
  24:              {
  25:                  throw new ArgumentException("Parameters 'web' or 'listName' are required and shouldn't be null or empty");
  26:              }
  27:   
  28:              if (!web.Lists.SovTryGet(listName, out this.quizzList))
  29:              {
  30:                  throw new ArgumentOutOfRangeException(
  31:                      String.Format(
  32:                      CultureInfo.InvariantCulture,
  33:                      "The list {0} doesn't exist inside the website '{1}'",
  34:                      listName,
  35:                      web.Title));
  36:              }
  37:          }
  38:          #endregion
  39:   
  40:          #region Public Methods
  41:          /// <summary>
  42:          /// Gets the next question.
  43:          /// </summary>
  44:          /// <param name="index">The index.</param>
  45:          /// <param name="category">The category.</param>
  46:          /// <returns>A Question</returns>
  47:          /// <exception cref="System.ArgumentException">Thrown when index is lower than 0</exception>
  48:          public Question GetNextQuestion(int index, Category category)
  49:          {
  50:              if (index < 0)
  51:              {
  52:                  throw new ArgumentException("GetNextQuestion : index is lower than 0");
  53:              }
  54:   
  55:              Question question = null;
  56:   
  57:              StringBuilder queryQuizzBuilder = new StringBuilder();
  58:   
  59:              queryQuizzBuilder.Append("         <OrderBy>");
  60:              queryQuizzBuilder.Append("              <FieldRef Name=\"ID\" />");
  61:              queryQuizzBuilder.Append("         </OrderBy>");
  62:              queryQuizzBuilder.Append("         <Where>");
  63:   
  64:              if (category != null && category.Id > 0)
  65:              {
  66:                  queryQuizzBuilder.Append("              <And>");
  67:                  queryQuizzBuilder.Append("                   <Eq>");
  68:                  queryQuizzBuilder.Append("                        <FieldRef Name=\"QuizzCategory\" LookupId=\"TRUE\" />");
  69:                  queryQuizzBuilder.AppendFormat("                        <Value Type=\"Lookup\">{0}</Value>", category.Id);
  70:                  queryQuizzBuilder.Append("                   </Eq>");
  71:              }
  72:   
  73:              queryQuizzBuilder.Append("                   <Gt>");
  74:              queryQuizzBuilder.Append("                        <FieldRef Name=\"ID\" />");
  75:              queryQuizzBuilder.AppendFormat("                        <Value Type=\"Counter\">{0}</Value>", index);
  76:              queryQuizzBuilder.Append("                   </Gt>");
  77:   
  78:              if (category != null && category.Id > 0)
  79:              {
  80:                  queryQuizzBuilder.Append("              </And>");
  81:              }
  82:   
  83:              queryQuizzBuilder.Append("         </Where>");
  84:   
  85:              string queryQuizzCaml = queryQuizzBuilder.ToString();
  86:   
  87:              SPQuery queryQuizz = new SPQuery();
  88:              queryQuizz.Query = queryQuizzCaml;
  89:              queryQuizz.RowLimit = 1;
  90:   
  91:              SPListItemCollection resultQuestion = this.quizzList.GetItems(queryQuizz);
  92:   
  93:              if (resultQuestion.Count > 0)
  94:              {
  95:                  SPListItem itemQuizz = resultQuestion[0];
  96:   
  97:                  question = new Question
  98:                  {
  99:                      Id = (int)itemQuizz[SPBuiltInFieldId.ID],
 100:                      Content = itemQuizz[Fields.QuizzQuestion] as string,
 101:                      Answers = this.GetAnswersForQuestion(itemQuizz),
 102:                      Category = this.GetCategoryForQuestion(itemQuizz)
 103:                  };
 104:              }
 105:   
 106:              return question;
 107:          }
 108:   
 109:          /// <summary>
 110:          /// Gets the next question.
 111:          /// </summary>
 112:          /// <param name="index">The index.</param>
 113:          /// <returns>A Question</returns>
 114:          /// <exception cref="System.ArgumentException">Throw when index is lower than 0</exception>
 115:          public Question GetNextQuestion(int index)
 116:          {
 117:              return this.GetNextQuestion(index, null);
 118:          }
 119:   
 120:          /// <summary>
 121:          /// Gets the question.
 122:          /// </summary>
 123:          /// <param name="index">The index.</param>
 124:          /// <returns>A Question</returns>
 125:          /// <exception cref="System.ArgumentException">Thrown when index is lower than 0</exception>
 126:          public Question GetQuestion(int index)
 127:          {
 128:              if (index < 0)
 129:              {
 130:                  throw new ArgumentException("GetQuestion : index is lower than 0");
 131:              }
 132:   
 133:              Question question = null;
 134:   
 135:              StringBuilder queryQuizzBuilder = new StringBuilder();
 136:   
 137:              queryQuizzBuilder.Append("         <Where>");
 138:              queryQuizzBuilder.Append("                   <Eq>");
 139:              queryQuizzBuilder.Append("                        <FieldRef Name=\"ID\" />");
 140:              queryQuizzBuilder.AppendFormat("                        <Value Type=\"Counter\">{0}</Value>", index);
 141:              queryQuizzBuilder.Append("                   </Eq>");
 142:   
 143:              queryQuizzBuilder.Append("         </Where>");
 144:   
 145:              string queryQuizzCaml = queryQuizzBuilder.ToString();
 146:   
 147:              SPQuery queryQuizz = new SPQuery();
 148:              queryQuizz.Query = queryQuizzCaml;
 149:              queryQuizz.RowLimit = 1;
 150:   
 151:              SPListItemCollection resultQuestion = this.quizzList.GetItems(queryQuizz);
 152:   
 153:              if (resultQuestion.Count > 0)
 154:              {
 155:                  SPListItem itemQuizz = resultQuestion[0];
 156:   
 157:                  question = new Question
 158:                  {
 159:                      Id = (int)itemQuizz[SPBuiltInFieldId.ID],
 160:                      Content = itemQuizz[Fields.QuizzQuestion] as string,
 161:                      Answers = this.GetAnswersForQuestion(itemQuizz),
 162:                      Category = this.GetCategoryForQuestion(itemQuizz)
 163:                  };
 164:              }
 165:   
 166:              return question;
 167:          }
 168:          #endregion
 169:      }
 170:  }

PS : l'objet Question est un objet métier crée par mes soins, composé d'un ID, d'un Contenu, de Réponses (définit par leur contenu et leur validité) et d'un Catégorie.

Bon alors tout ça, c'est très bien mais comment être réellement sur que ça fonctionne, que mes requêtes sont correctes, etc...

Et pourquoi on ne ferait pas des test unitaires qui nous permettraient de valider ces méthodes ?

image

Ok pourquoi pas, mais comment ? Et bien regardons déjà pour la première méthode : GetNextQuestionShouldReturnFirstQuestion.

   1:  using System.Collections.Generic;
   2:  using Microsoft.SharePoint;
   3:  using Microsoft.VisualStudio.TestTools.UnitTesting;
   4:  using Microsoft.VisualStudio.TestTools.UnitTesting.Web;
   5:  using TechDays.Demo.SharePoint.BusinessEntities;
   6:   
   7:  namespace TechDays.Demo.SharePoint.Model.Tests
   8:  {
   9:      /// <summary>
  10:      /// Test Class that will handle all the Tests for the Question Repository
  11:      /// </summary>
  12:      [TestClass]
  13:      public class QuestionRepositoryTests
  14:      {
  15:          /// <summary>
  16:          /// List of Questions
  17:          /// </summary>
  18:          private readonly List<Question> questions;
  19:   
  20:          /// <summary>
  21:          /// Initializes a new instance of the QuestionRepositoryTests class.
  22:          /// </summary>
  23:          public QuestionRepositoryTests()
  24:          {
  25:              this.questions = new List<Question>();
  26:   
  27:              #region Test Collection Initialization
  28:   
  29:              Question q1 = new Question()
  30:              {
  31:                  Id = 1,
  32:                  Content = "Dans quel pays, le chat domestique était vénéré à l'époque des pharaons ?",
  33:                  Category = new Category() { Id = 1, Title = "Carnivore" },
  34:                  Answers = new List<Answer>() 
  35:                  { 
  36:                      new Answer() { Content = "Egypte", IsValid = true }, 
  37:                      new Answer() { Content = "Tunisie", IsValid = false },
  38:                      new Answer() { Content = "Espagne", IsValid = false }
  39:                  }
  40:              };
  41:   
  42:              Question q2 = new Question()
  43:              {
  44:                  Id = 2,
  45:                  Content = "Quel est le reptile le plus grand ?",
  46:                  Category = new Category() { Id = 3, Title = "Reptile" },
  47:                  Answers = new List<Answer>() 
  48:                  { 
  49:                      new Answer() { Content = "Cochon d'inde", IsValid = false },
  50:                      new Answer() { Content = "Autruche", IsValid = false },
  51:                      new Answer() { Content = "Crocodile", IsValid = true }
  52:                  }
  53:              }; 
  54:              #endregion
  55:   
  56:              this.questions.Add(q1);
  57:              this.questions.Add(q2);
  58:          }
  59:   
  60:          /// <summary>
  61:          /// When GetNextQuestion(0) is called, it should return the first question.
  62:          /// </summary>
  63:          [TestMethod]
  64:          [HostType("ASP.Net")]
  65:          [UrlToTest("http://localhost/default.aspx")]
  66:          public void GetNextQuestionShouldReturnFirstQuestion()
  67:          {
  68:              Question expected = this.questions[0];
  69:              string listName = "Quizz Animaux";
  70:   
  71:              QuestionRepository repository = new QuestionRepository(SPContext.Current.Web, listName);
  72:   
  73:              Question result = repository.GetNextQuestion(0);
  74:   
  75:              Assert.AreEqual(expected.Content, result.Content);
  76:              Assert.AreEqual(expected.Category.Title, result.Category.Title);
  77:              Assert.AreEqual(expected.Answers.Count, result.Answers.Count);
  78:              Assert.AreEqual(expected.Answers[0].Content, result.Answers[0].Content);
  79:              Assert.AreEqual(expected.Answers[0].IsValid, result.Answers[0].IsValid);
  80:              Assert.AreEqual(expected.Answers[1].Content, result.Answers[1].Content);
  81:              Assert.AreEqual(expected.Answers[1].IsValid, result.Answers[1].IsValid);
  82:              Assert.AreEqual(expected.Answers[2].Content, result.Answers[2].Content);
  83:              Assert.AreEqual(expected.Answers[2].IsValid, result.Answers[2].IsValid);
  84:          }
  85:      }
  86:  }

Petit rappel important, pour que ce test fonctionne il faut savoir que j'ai actuellement un environnement de test déjà créé sur ma machine de développement (cf post précédent à ce sujet) avec toutes les données de tests dont j'ai besoin.(cf image ci-dessous)

image

Dans le constructeur QuestionRepositoryTests, j'initialise simplement une collection de réponses attendues afin que ce soit plus pratique à manipuler dans mes méthodes de tests.

Dans la méthode de test GetNextQuestionShouldReturnFirstQuestion, j'instancie mon repository "normalement", appelle la méthode GetNextQuestion "normalement" et vérifie simplement que la réponse récupérée est bien celle attendue.

La petite particularité se situe au niveau des attributs de la méthode de test GetNextQuestionShouldReturnFirstQuestion : [HostType("ASP.Net")] et [UrlToTest(http://localhost/default.aspx)]. Ces attributs me permettent de m'attacher à l'environnement SharePoint qui est sur ma machine de développement et faire les requêtes CAML comme si j'étais dans le contexte SharePoint.

Le seul inconvénient majeur étant, bien entendu, la dépendance sur l'environnement de test qui est toujours discutable pour les puristes.

Dans le prochain post, vous verrez qu'il est possible de faire des tests unitaires avec TypeMock qui nous permettront d'eviter cette dépendance.

<Philippe/>

SharePoint 2007 : MVP SharePoint !! again :)

image

C'est avec beaucoup de plaisir que j'ai appris ma nomination MVP SharePoint.

Du coup, la joyeuse bande de communautaires SharePoint devra me supporter une année de plus :)

image 

De gauche à droite, une toute petite partie de la French Team au MVP Summit 2009 : Sébastien Picamelot, moi-même, Renaud Comte, Fabrice Romelard, Gaetan Bouveret.

En tout cas, une grand merci à toutes les personnes, amis, collègues et/ou autres, qui m'ont apportées leur soutien tout au long de l'année.

PS : J'en profite pour passer une petite dédicace à Etienne Margraff pour sa nomination MVP.

image 

Félicitations Etienne !! C'est mérité.

<Philippe/>

SharePoint 2007 : SPDisposeCheck ne vérifierait pas tout les patterns de dispose connus

[via Stephen Vick’s Blog ]

Dans la série des nouvelles qui ne font pas vraiment plaisir, il semblerait que SPDisposeCheck ne vérifie pas tout les patterns de Dispose présentés dans les liens suivants :

En effet, Stephen Vick montre que SPDisposeCheck ne vérifie pas les méthodes suivantes dans le fichier utilisé (basé sur le post de Roger Lamb) :

  • CreatingSPSiteLeak
  • AllWebsForEachLeak
  • AllWebsIndexerLeak
  • SPLimitedWebPartManagerLeak
  • WebsLeak
  • PublishingWebCollectionLeak
  • GetVariationLeak
  • PersonalSiteLeak
  • SPSiteCollectionIndexerLeak
  • SPSiteCollectionForEachLeak
  • CrossMethodLeak.MethodB
  • CrossMethodLeak.MethodC
Ce qui fait quand même une petite tripoté de méthodes et d'objets qui ne sont pas vérifiés.

Voilà le fichier en question :

   1:  namespace SPDisposeTest
   2:  {
   3:      public class Class1
   4:      {
   5:          void CreatingSPSiteLeak()
   6:          {
   7:              SPSite siteCollection = new SPSite("http://moss");
   8:              // siteCollection leaked
   9:          }
  10:   
  11:          void CreatingSPSiteExplicitDisposeNoLeak()
  12:          {
  13:              SPSite siteCollection = null;
  14:              try
  15:              {
  16:                  siteCollection = new SPSite("http://moss");
  17:              }
  18:              finally
  19:              {
  20:                  if (siteCollection != null)
  21:                      siteCollection.Dispose();
  22:              }
  23:          }
  24:   
  25:          void CreatingSPSiteWithAutomaticDisposeNoLeak()
  26:          {
  27:              using (SPSite siteCollection = new SPSite("http://moss"))
  28:              {
  29:              } // SPSite object siteCollection.Dispose() automatically called
  30:          }
  31:   
  32:          void OpenWebLeak()
  33:          {
  34:              using (SPWeb web = new SPSite(SPContext.Current.Web.Url).OpenWeb())
  35:              {
  36:                  // SPSite leaked !
  37:              } // SPWeb object web.Dispose() automatically called
  38:          }
  39:   
  40:          void OpenWebNoLeak()
  41:          {
  42:              using (SPSite siteCollection = new SPSite("http://moss"))
  43:              {
  44:                  using (SPWeb web = siteCollection.OpenWeb())
  45:                  {
  46:                  } // SPWeb object web.Dispose() automatically called
  47:              } // SPSite object siteCollection.Dispose() automatically called
  48:          }
  49:   
  50:          void AllWebsForEachLeak()
  51:          {
  52:              using (SPSite siteCollection = new SPSite("http://moss"))
  53:              {
  54:                  using (SPWeb outerWeb = siteCollection.OpenWeb())
  55:                  {
  56:                      foreach (SPWeb innerWeb in siteCollection.AllWebs)
  57:                      {
  58:                          // explicit dispose here to avoid OOM’s with large # of webs
  59:                      }
  60:                  } // SPWeb object outerWeb.Dispose() automatically called
  61:              } // SPSite object siteCollection.Dispose() automatically called
  62:          }
  63:   
  64:          void AllWebsForEachNoLeakOrMemoryOOM()
  65:          {
  66:              using (SPSite siteCollection = new SPSite("http://moss"))
  67:              {
  68:                  using (SPWeb outerWeb = siteCollection.OpenWeb())
  69:                  {
  70:                      foreach (SPWeb innerWeb in siteCollection.AllWebs)
  71:                      {
  72:                          try
  73:                          {
  74:                              // …
  75:                          }
  76:                          finally
  77:                          {
  78:                              if (innerWeb != null)
  79:                                  innerWeb.Dispose();
  80:                          }
  81:                      }
  82:                  } // SPWeb object outerWeb.Dispose() automatically called
  83:              } // SPSite object siteCollection.Dispose() automatically called
  84:          }
  85:   
  86:          void AllWebsIndexerLeak()
  87:          {
  88:              using (SPSite siteCollection = new SPSite("http://moss"))
  89:              {
  90:                  SPWeb web = siteCollection.AllWebs[0];
  91:                  // SPWeb web leaked
  92:              } // SPSite object siteCollection.Dispose() automatically called
  93:          }
  94:   
  95:          void AllWebsIndexerNoLeak()
  96:          {
  97:              using (SPSite siteCollection = new SPSite("http://moss"))
  98:              {
  99:                  using (SPWeb web = siteCollection.AllWebs[0])
 100:                  {
 101:                  } // SPWeb object web.Dispose() automatically called
 102:              } // SPSite object siteCollection.Dispose() automatically called
 103:          }
 104:   
 105:          void AllWebsAddLeak()
 106:          {
 107:              using (SPSite siteCollection = new SPSite("http://moss"))
 108:              {
 109:                  SPWeb web = siteCollection.AllWebs.Add("site-relative URL");
 110:                  // SPWeb web Leaked
 111:              } // SPSite object siteCollection.Dispose() automatically called
 112:          }
 113:   
 114:          void AllWebsAddNoLeak()
 115:          {
 116:              using (SPSite siteCollection = new SPSite("http://moss"))
 117:              {
 118:                  using (SPWeb web = siteCollection.AllWebs.Add("site-relative URL"))
 119:                  {
 120:                  } // SPWeb object web.Dispose() automatically called
 121:              } // SPSite object siteCollection.Dispose() automatically called
 122:          }
 123:   
 124:          void SPLimitedWebPartManagerLeak()
 125:          {
 126:              using (SPSite siteCollection = new SPSite("http://moss"))
 127:              {
 128:                  using (SPWeb web = siteCollection.OpenWeb())
 129:                  {
 130:                      SPFile page = web.GetFile("Source_Folder_Name/Source_Page");
 131:                      SPLimitedWebPartManager webPartManager = page.GetLimitedWebPartManager(PersonalizationScope.Shared);
 132:                      // SPWeb object webPartManager.Web leaked
 133:                  } // SPWeb object web.Dispose() automatically called
 134:              } // SPSite object siteCollection.Dispose() automatically called
 135:          }
 136:   
 137:          void SPLimitedWebPartManagerNoLeak()
 138:          {
 139:              using (SPSite siteCollection = new SPSite("http://moss"))
 140:              {
 141:                  using (SPWeb web = siteCollection.OpenWeb())
 142:                  {
 143:                      SPFile page = web.GetFile("Source_Folder_Name/Source_Page");
 144:                      using (SPLimitedWebPartManager webPartManager = page.GetLimitedWebPartManager(PersonalizationScope.Shared))
 145:                      {
 146:                          try
 147:                          {
 148:                              // …
 149:                          }
 150:                          finally
 151:                          {
 152:                              webPartManager.Web.Dispose();
 153:                          }
 154:                      }
 155:                  } // SPWeb object web.Dispose() automatically called
 156:              } // SPSite object siteCollection.Dispose() automatically called
 157:          }
 158:   
 159:          void WebsLeak()
 160:          {
 161:              using (SPSite siteCollection = new SPSite("http://moss"))
 162:              {
 163:                  using (SPWeb outerWeb = siteCollection.OpenWeb())
 164:                  {
 165:                      foreach (SPWeb innerWeb in outerWeb.Webs)
 166:                      {
 167:                          // SPWeb innerWeb leak
 168:                      }
 169:                  } // SPWeb object outerWeb.Dispose() automatically called
 170:              } // SPSite object siteCollection.Dispose() automatically called
 171:          }
 172:   
 173:          void WebsNoLeak()
 174:          {
 175:              using (SPSite siteCollection = new SPSite("http://moss"))
 176:              {
 177:                  using (SPWeb outerWeb = siteCollection.OpenWeb())
 178:                  {
 179:                      foreach (SPWeb innerWeb in outerWeb.Webs)
 180:                      {
 181:                          try
 182:                          //should be 1st statement after foreach
 183:                          {
 184:                              // …
 185:                          }
 186:                          finally
 187:                          {
 188:                              if (innerWeb != null)
 189:                                  innerWeb.Dispose();
 190:                          }
 191:                      }
 192:                  } // SPWeb object outerWeb.Dispose() automatically called
 193:              } // SPSite object siteCollection.Dispose() automatically called
 194:          }
 195:   
 196:          void WebsAddLeak(string strWebUrl)
 197:          {
 198:              using (SPSite siteCollection = new SPSite("http://moss"))
 199:              {
 200:                  using (SPWeb web = siteCollection.OpenWeb())
 201:                  {
 202:                      SPWeb addedWeb = web.Webs.Add(strWebUrl); // will leak
 203:                  } // SPWeb object web.Dispose() automatically called
 204:              } // SPSite object siteCollection.Dispose() automatically called
 205:          }
 206:   
 207:          void WebsAddNoLeak(string strWebUrl)
 208:          {
 209:              using (SPSite siteCollection = new SPSite("http://moss"))
 210:              {
 211:                  using (SPWeb web = siteCollection.OpenWeb())
 212:                  {
 213:                      using (SPWeb addedWeb = web.Webs.Add(strWebUrl))
 214:                      {
 215:                          //..
 216:                      }
 217:                  } // SPWeb object web.Dispose() automatically called
 218:              } // SPSite object siteCollection.Dispose() automatically called
 219:          }
 220:   
 221:          void SPWebCollectionAddLeak(string strWebUrl)
 222:          {
 223:              using (SPSite siteCollection = new SPSite("http://moss"))
 224:              {
 225:                  using (SPWeb outerWeb = siteCollection.OpenWeb())
 226:                  {
 227:                      SPWebCollection webCollection = siteCollection.AllWebs; // no AllWebs leak just getting reference
 228:                      SPWeb innerWeb = webCollection.Add(strWebUrl); // must dispose of innerWeb
 229:                      // innerWeb Leak
 230:                  } // SPWeb object outerWeb.Dispose() automatically called
 231:              } // SPSite object siteCollection.Dispose() automatically called
 232:          }
 233:   
 234:          void SPWebCollectionAddNoLeak(string strWebUrl)
 235:          {
 236:              using (SPSite siteCollection = new SPSite("http://moss"))
 237:              {
 238:                  using (SPWeb outerWeb = siteCollection.OpenWeb())
 239:                  {
 240:                      SPWebCollection webCollection = siteCollection.AllWebs; // no AllWebs leak just getting reference
 241:                      using (SPWeb innerWeb = webCollection.Add(strWebUrl))
 242:                      {
 243:                          //…
 244:                      }
 245:                  } // SPWeb object outerWeb.Dispose() automatically called
 246:              } // SPSite object siteCollection.Dispose() automatically called
 247:          }
 248:   
 249:          void SPControlBADPractice()
 250:          {
 251:              HttpContext Context = null;
 252:              SPSite siteCollection = SPControl.GetContextSite(Context);
 253:              siteCollection.Dispose(); // DO NOT DO THIS
 254:              SPWeb web = SPControl.GetContextWeb(Context);
 255:              web.Dispose(); // DO NOT DO THIS
 256:          }
 257:   
 258:          void SPControlBestPractice()
 259:          {
 260:              HttpContext Context = null;
 261:              SPSite siteCollection = SPControl.GetContextSite(Context);
 262:              SPWeb web = SPControl.GetContextWeb(Context);
 263:              // Do NOT call Dispose()
 264:          }
 265:   
 266:          void SPContextBADPractice()
 267:          {
 268:              SPSite siteCollection = SPContext.Current.Site;
 269:              siteCollection.Dispose(); // DO NOT DO THIS
 270:              SPWeb web = SPContext.Current.Web;
 271:              web.Dispose(); // DO NOT DO THIS
 272:          }
 273:   
 274:          void SPContextBestPractice()
 275:          {
 276:              SPSite siteCollection = SPContext.Current.Site;
 277:              SPWeb web = SPContext.Current.Web;
 278:              // Do NOT call Dispose()
 279:          }
 280:   
 281:          void PublishingWebCollectionLeak()
 282:          {
 283:              using (SPSite siteCollection = new SPSite("http://moss"))
 284:              {
 285:                  using (SPWeb web = siteCollection.OpenWeb())
 286:                  {
 287:                      // passing in web you own, no dispose needed on outerPubWeb
 288:                      PublishingWeb outerPubWeb = PublishingWeb.GetPublishingWeb(web);
 289:   
 290:                      PublishingWebCollection pubWebCollection = outerPubWeb.GetPublishingWebs();
 291:                      foreach (PublishingWeb innerPubWeb in pubWebCollection)
 292:                      {
 293:                          // innerPubWeb leak
 294:                      }
 295:   
 296:                      // PublishingWeb will leak for each innerPubWeb referenced
 297:                  } // SPWeb object web.Dispose() automatically called
 298:              } // SPSite object siteCollection.Dispose() automatically called
 299:          }
 300:   
 301:          void PublishingWebCollectionNoLeak()
 302:          {
 303:              using (SPSite siteCollection = new SPSite("http://moss"))
 304:              {
 305:                  using (SPWeb web = siteCollection.OpenWeb())
 306:                  {
 307:                      // passing in web you own, no dispose needed on outerPubWeb
 308:                      PublishingWeb outerPubWeb = PublishingWeb.GetPublishingWeb(web);
 309:                      PublishingWebCollection pubWebCollection = outerPubWeb.GetPublishingWebs();
 310:                      foreach (PublishingWeb innerPubWeb in pubWebCollection)
 311:                      {
 312:                          try
 313:                          {
 314:                              // …
 315:                          }
 316:                          finally
 317:                          {
 318:                              if (innerPubWeb != null)
 319:                                  innerPubWeb.Close();
 320:                          }
 321:                      }
 322:                      // outerPubWeb.Close(); not needed and if called will log warning in ULS log
 323:                  } // SPWeb object web.Dispose() automatically called
 324:              } // SPSite object siteCollection.Dispose() automatically called
 325:          }
 326:   
 327:          void GetPublishingWebNoLeak()
 328:          {
 329:              using (SPSite siteCollection = new SPSite("http://moss"))
 330:              {
 331:                  using (SPWeb web = siteCollection.OpenWeb())
 332:                  {
 333:                      // passing in web you own, no dispose needed on singlePubWeb
 334:                      PublishingWeb singlePubWeb = PublishingWeb.GetPublishingWeb(web);
 335:                  } // SPWeb object web.Dispose() automatically called
 336:              } // SPSite object siteCollection.Dispose() automatically called
 337:          }
 338:   
 339:          void GetVariationLeak()
 340:          {
 341:              using (SPSite siteCollection = new SPSite("http://moss"))
 342:              {
 343:                  using (SPWeb web = siteCollection.OpenWeb())
 344:                  {
 345:                      PublishingWeb publishingWeb = PublishingWeb.GetPublishingWeb(web); // Passing in web so no Close() needed
 346:                      VariationLabel variationLabel = Variations.Current.UserAccessibleLabels[0];
 347:                      PublishingWeb variationPublishingWeb = publishingWeb.GetVariation(variationLabel); // must be Closed()
 348:                      // …
 349:                  } // SPWeb object web.Dispose() automatically called
 350:              } // SPSite object siteCollection.Dispose() automatically called
 351:          }
 352:   
 353:          void GetVariationNoLeak()
 354:          {
 355:              using (SPSite siteCollection = new SPSite("http://moss"))
 356:              {
 357:                  using (SPWeb web = siteCollection.OpenWeb())
 358:                  {
 359:                      PublishingWeb variationPublishingWeb = null;
 360:                      try
 361:                      {
 362:                          PublishingWeb publishingWeb = PublishingWeb.GetPublishingWeb(web); // Passing in web so no Close() needed
 363:                          VariationLabel variationLabel = Variations.Current.UserAccessibleLabels[0];
 364:                          variationPublishingWeb = publishingWeb.GetVariation(variationLabel); // must be Closed()
 365:                          // …
 366:                      }
 367:                      finally
 368:                      {
 369:                          if (variationPublishingWeb != null)
 370:                              variationPublishingWeb.Close();
 371:                      }
 372:                  } // SPWeb object outerWeb.Dispose() automatically called
 373:              } // SPSite object siteCollection.Dispose() automatically called
 374:          }
 375:   
 376:          void PersonalSiteLeak()
 377:          {
 378:              // open a site collection
 379:              using (SPSite siteCollection = new SPSite("http://moss"))
 380:              {
 381:                  UserProfileManager profileManager = new UserProfileManager(ServerContext.GetContext(siteCollection));
 382:                  UserProfile profile = profileManager.GetUserProfile("domain\\username");
 383:                  SPSite personalSite = profile.PersonalSite; // will leak
 384:              }
 385:          }
 386:   
 387:          void PersonalSiteNoLeak()
 388:          {
 389:              // open a site collection
 390:              using (SPSite siteCollection = new SPSite("http://moss"))
 391:              {
 392:                  UserProfileManager profileManager = new UserProfileManager(ServerContext.GetContext(siteCollection));
 393:                  UserProfile profile = profileManager.GetUserProfile("domain\\username");
 394:                  using (SPSite personalSite = profile.PersonalSite)
 395:                  {
 396:   
 397:                      // …
 398:                  }
 399:              }
 400:          }
 401:   
 402:          void SPSiteCollectionIndexerLeak()
 403:          {
 404:              using (SPSite siteCollectionOuter = new SPSite("http://moss"))
 405:              {
 406:                  SPWebApplication webApp = siteCollectionOuter.WebApplication;
 407:                  SPSiteCollection siteCollections = webApp.Sites;
 408:   
 409:   
 410:                  SPSite siteCollectionInner = siteCollections[0];
 411:                  // SPSite siteCollectionInner leak
 412:              } // SPSite object siteCollectionOuter.Dispose() automatically called
 413:          }
 414:   
 415:          void SPSiteCollectionIndexerNoLeak()
 416:          {
 417:              using (SPSite siteCollectionOuter = new SPSite("http://moss"))
 418:              {
 419:                  SPSite siteCollectionInner = null;
 420:                  try
 421:                  {
 422:                      SPWebApplication webApp = siteCollectionOuter.WebApplication;
 423:                      SPSiteCollection siteCollections = webApp.Sites;
 424:   
 425:   
 426:                      siteCollectionInner = siteCollections[0];
 427:                  }
 428:                  finally
 429:                  {
 430:                      if (siteCollectionInner != null)
 431:                          siteCollectionInner.Dispose();
 432:                  }
 433:              } // SPSite object siteCollectionOuter.Dispose() automatically called
 434:          }
 435:   
 436:          void SPSiteCollectionForEachLeak()
 437:          {
 438:              using (SPSite siteCollectionOuter = new SPSite("http://moss"))
 439:              {
 440:                  SPWebApplication webApp = siteCollectionOuter.WebApplication;
 441:                  SPSiteCollection siteCollections = webApp.Sites;
 442:   
 443:                  foreach (SPSite siteCollectionInner in siteCollections)
 444:                  {
 445:                      // SPSite siteCollectionInner leak
 446:                  }
 447:              } // SPSite object siteCollectionOuter.Dispose() automatically called
 448:          }
 449:   
 450:          void SPSiteCollectionForEachNoLeak()
 451:          {
 452:              using (SPSite siteCollectionOuter = new SPSite("http://moss"))
 453:              {
 454:                  SPWebApplication webApp = siteCollectionOuter.WebApplication;
 455:                  SPSiteCollection siteCollections = webApp.Sites;
 456:   
 457:   
 458:                  foreach (SPSite siteCollectionInner in siteCollections)
 459:                  {
 460:                      try
 461:                      {
 462:                          // …
 463:                      }
 464:                      finally
 465:                      {
 466:                          if (siteCollectionInner != null)
 467:                              siteCollectionInner.Dispose();
 468:                      }
 469:                  }
 470:              } // SPSite object siteCollectionOuter.Dispose() automatically called
 471:          }
 472:   
 473:          void SPSiteCollectionAddLeak()
 474:          {
 475:              SPWebApplication webApp = new SPSite("http://moss").WebApplication;
 476:              SPSiteCollection siteCollections = webApp.Sites;
 477:              SPSite siteCollection = siteCollections.Add("sites/myNewSiteCollection", "DOMAIN\\User", "roger.lamb@litwareinc.com");
 478:              // SPSite siteCollection leak
 479:          }
 480:   
 481:          void SPSiteCollectionAddNoLeak()
 482:          {
 483:              SPWebApplication webApp = new SPSite("http://moss").WebApplication;
 484:              SPSiteCollection siteCollections = webApp.Sites;
 485:              using (SPSite siteCollection = siteCollections.Add("sites/myNewSiteCollection", "DOMAIN\\User", "roger.lamb@litwareinc.com"))
 486:              {
 487:              } // SPSite object siteCollection.Dispose() automatically called
 488:          }
 489:   
 490:          public class CrossMethodLeak
 491:          {
 492:              private SPSite _siteCollection = null;
 493:              private SPWeb _web = null;
 494:   
 495:              public void MethodA()
 496:              {
 497:                  _siteCollection = new SPSite("http://moss");
 498:                  _web = _siteCollection.OpenWeb();
 499:              }
 500:   
 501:              public void MethodB()
 502:              {
 503:                  if (_web != null)
 504:                  {
 505:                      string title = _web.Title;
 506:                  }
 507:              }
 508:   
 509:              public void MethodC()
 510:              {
 511:                  if (_web != null)
 512:                  {
 513:                      string name = _web.Name;
 514:                  }
 515:              }
 516:          }
 517:      }
 518:  }

Alors un conseil, en attendant que ce soit corrigé/confirmé, ne considérez pas SPDisposeCheck comme parole d'évangile.

EDIT :Selon Paul Andrew, Il semblerait que toutes les erreurs soient détectées si vous compilez votre code en release. Une future release est à prévoir qui corrigerait toute ses erreurs.

<Philippe/>

SharePoint 2007 : Vidéos des Sessions aux Techdays 2009

Après un bon mois de repos coté blogosphère, il est tant de se remettre au "travail".

Je viens de voir que quelques sessions SharePoint viennent d'être publiées sur le site des Techdays 2009. Je mettrais ce post à jour au fur et à mesure des nouvelles publications.

Pour l'instant, on retrouve 4 sessions pour "SharePointeurs" :

[Microsoft TechDays 2009] - Mettez un turbo dans vos applications SharePoint - 1/3

Auteur : Renaud Comte - Niveau : Confirmé (300)

image

[Microsoft TechDays 2009] - Réalisation de son site e-commerce avec Commerce Server et Sharepoint Server - 1/3

Auteur : Guillaume Beaumont - Niveau : Confirmé (300)

image

[Microsoft TechDays 2009] - Industrialisation du développement SharePoint avec Team System - 1/3

Auteur : Adrien Siffermann & Philippe Sentenac - Niveau : Intermédiaire (200)

image

[Microsoft TechDays 2009] - Introduction au développement SharePoint et cas pratiques - 1/3

Auteur : Gaëtan Bouveret & Sébastien Picamelot - Niveau : Découverte (100)

image

Bonne Visionnage !

<Philippe/>

SharePoint 2007 : Ambiance Techdays et Aperçu de notre session sur l'Industrialisation de SharePoint

La première des 3 journées dédiée aux Techdays vient de commencer et le moins que l'on puisse dire, c'est que tout le monde est au rendez vous !

003 004 009

Mais bon... pendant ce temps, vos speakers répètent une dernière fois leur session.

Ci dessous Arnaud Auroux, Adrien Siffermann et Etienne Margraff qui peaufinent leurs slides dans la salle Speakers.

005 006

ici, Sebastien Picamelot et Gaétan Bouveret qui se détendent avant leur session "Introduction au développement SharePoint et cas pratiques" avec Julien Chomarat et François Rebour.

008  IMG_0031

Bon moi aussi j'y retourne, notre session Industrialisation des développement SharePoint avec Team System n'attend pas !

Je vous ai promis un aperçu de notre session alors voilà le premier slide :)

image

Venez nombreux, nous sommes en salle 241 sur le créneau de 16h00 à 17h00.

<Philippe/>

SharePoint 2007 : Dispose Patterns et l'outil SPDisposeCheck

[via Paul Andrew]

SPDisposeCheck avait été présenté dernièrement  sur le blog de l'équipe SharePoint dans un post relativement récent : Announcing SPDisposeCheck tool for SharePoint Developers

C'est un outil qui va vous permettre de vérifier que vous libérez bien les ressources liées à différents objets SharePoint comme SPSite, SPWeb, SPLimitedWebPartManager.Web, etc...  dans tout vos développements SharePoint.

En fait, il va s'assurer que vous respectez bien les bonnes pratiques spécifiés dans les posts suivants :

Par exemple, il va analyser votre code pour que vous évitiez de faire ce genre de choses (extrait du post de Roger Lamb's):

   1:  void SPLimitedWebPartManagerLeak()
   2:  {
   3:      using (SPSite siteCollection = new SPSite("http://moss"))
   4:      {
   5:          using (SPWeb web = siteCollection.OpenWeb())
   6:          {
   7:              SPFile page = web.GetFile("Source_Folder_Name/Source_Page");
   8:              SPLimitedWebPartManager webPartManager = page.GetLimitedWebPartManager(PersonalizationScope.Shared);
   9:              // SPWeb object webPartManager.Web leaked
  10:          } // SPWeb object web.Dispose() automatically called
  11:      }  // SPSite object siteCollection.Dispose() automatically called 
  12:  }
  13:   
  14:  void SPLimitedWebPartManagerNoLeak()
  15:  {
  16:      using (SPSite siteCollection = new SPSite("http://moss"))
  17:      {
  18:          using (SPWeb web = siteCollection.OpenWeb())
  19:          {
  20:              SPFile page = web.GetFile("Source_Folder_Name/Source_Page");
  21:              using (SPLimitedWebPartManager webPartManager = page.GetLimitedWebPartManager(PersonalizationScope.Shared))
  22:              {
  23:                  try
  24:                  {
  25:                      // ...
  26:                  }
  27:                  finally
  28:                  {
  29:                      webPartManager.Web.Dispose();
  30:                  }
  31:              }
  32:          } // SPWeb object web.Dispose() automatically called
  33:      }  // SPSite object siteCollection.Dispose() automatically called 
  34:  }

L'avantage évident de cet outil est d'être sur d'obtenir une application performante qui ne consomme pas plus de mémoire que nécessaire et qui ne conserve pas indéfiniment des objets SharePoint qui vont maintenir des accès couteux à la base de données SharePoint.

Bref un seul conseil à vous donner : allez le récupérer et tester le, ça va vous changer la vie !

Vous pouvez le télécharger sur le site de la MSDN Code Gallery : http://code.msdn.microsoft.com/SPDisposeCheck

<Philippe/>

SharePoint 2007 : Création d'environnement SharePoint automatisée

Après en avoir fini avec toute la plomberie nécessaire au fonctionnement de l'application Quizz dans le dernier post, je vais enfin pouvoir vous parler du développement de l'application en elle-même.

Mais avant, ça je vous voulais faire un petit post pour vous présenter comment je déploie simplement l'application sur mon environnement de développement.

La plupart d'entre vous sont déjà en train de se dire que je vais vous parler de WSPBuilder ou de ce genre d'outil mais il n'en est rien.

Quand je vous parle de déploiement de l'application, je parle effectivement du déploiement de la solution mais plus encore :

  1. Upload de la Solution
  2. Création de la WebApplication
  3. Déploiement de la Solution
  4. Création de la SiteCollection
  5. Activation des Features
  6. Création de la liste Quizz Catégories
  7. Ajout de catégories dans la liste Quizz Catégories
  8. Création d'une liste Quizz Animaux basé sur notre ListDefinition
  9. Ajout de Questions à la liste Quizz Animaux.

L'idée étant d'obtenir un environnement complet en y passant le moins de temps possible.

Pour ceux qui surveillent la blogosphère SharePoint, vous devez savoir qu'il existe quelques outils qui me permettraient de réaliser certaines de ces opérations (mais pas toutes) :

Malheureusement Adrien et moi même souhaitions un outil qui puisse être simplement intégré à un Server de Build et à une intégration continue dans le cadre d'un projet SharePoint. Ce qui impliquait non seulement la création d'un environnement complet mais aussi son nettoyage. Du coup, nous l'avons développé nous-même :)

Au final, sans rentrer dans les détails, l'outil se base sur le modèle objet SharePoint et un fichier XML pour recréer tout l'environnement. L'outil se présente sous la forme d'une partie client et une partie serveur qui communiqueront ensemble via WCF. Le serveur en lui-même est un service Windows qui est installé sur la machine qui host SharePoint.

Concernant le projet Quizz dont je parle depuis quelques posts, le fichier XML de déploiement ressemble à ceci :

   1:  <?xml version="1.0" encoding="utf-8" ?>
   2:  <Farm xmlns="http://***/SharePointDataPopulate">
   3:    <Solutions>
   4:      <Solution Name="Demo.SharePoint.Quizz.wsp"
   5:                Path="C:\Users\Administrator\Documents\Visual Studio 2008\Projects\
   6:                Demo.SharePoint.Quizz\Quizz.Solution\Demo.SharePoint.Quizz.wsp"
   7:                IgnoreIfExists="true" />
   8:    </Solutions>
   9:    <WebApps>
  10:      <WebApp Port="80"
  11:              SQLPassword="Password0"
  12:              SQLUserName="SVC-SPCONF"
  13:              APidType="SpecificUser"
  14:              APidLogin="ORIJIN-HV\SVC-SPCONP"
  15:              APidPwd="Password0">
  16:        <Solutions>
  17:          <Solution Name="Demo.SharePoint.Quizz.wsp"
  18:                    Force="true"/>
  19:        </Solutions>
  20:        <Sites>
  21:          <Site Url="/"
  22:                Title="Intranet"
  23:                Description="Description Intranet"
  24:                LCID="1036"
  25:                WebTemplate="STS#1"
  26:                OwnerEmail="administrator@orijin.local"
  27:                OwnerLogin="ORIJIN-HV\Administrator"
  28:                OwnerName="Philippe Sentenac">
  29:            <Features>
  30:              <!--Quizz Features-->
  31:              <Feature Id="{321BBA1F-47A9-4B0F-BB17-3A34CFD58CCE}"
  32:                       IgnoreIfExists="true"/>
  33:             </Features>
  34:            <Web Description="NotUsed"
  35:                 Title="NotUsed"
  36:                 LCID="1033"
  37:                 UniquePermissions="false"
  38:                 Url="NotUsed"
  39:                 WebTemplate="NotUsed">
  40:              <Lists>
  41:                <List Title="Quizz Catégories"
  42:                      ListTemplate="Generic"
  43:                      Description="">
  44:                  <ListItems>
  45:                    <ListItem Title="Carnivore" />
  46:                    <ListItem Title="Herbivore" />
  47:                    <ListItem Title="Reptile" />
  48:                  </ListItems>
  49:                </List>
  50:   
  51:                <List Title="Quizz Animaux"
  52:                      ListTemplate="10042"
  53:                      Description=""
  54:                      FeatureId="{65E774A0-0B40-4341-B694-429B61A6EE15}"
  55:                      QuickLaunch="true"
  56:                      Url="Lists/QuizzAnimals">
  57:                  <ListItems>
  58:                    <ListItem Title="">
  59:                      <FieldsValues>
  60:                        <FieldValue FieldName="QuizzQuestion"><![CDATA[Dans quel pays, 
  61:                        le chat domestique était vénéré à l'époque des pharaons ?]]></FieldValue>
  62:                        <FieldValue FieldName="QuizzChoices"><![CDATA[Egypte;Tunisie;Espagne]]></FieldValue>
  63:                        <FieldValue FieldName="QuizzAnswers"><![CDATA[0]]></FieldValue>
  64:                        <FieldValue FieldName="QuizzCategory"><![CDATA[1;#Carnivore]]></FieldValue>
  65:                      </FieldsValues>
  66:                    </ListItem>
  67:                    <ListItem Title="">
  68:                      <FieldsValues>
  69:                        <FieldValue FieldName="QuizzQuestion"><![CDATA[quelle est le reptile le plus grand ?]]></FieldValue>
  70:                        <FieldValue FieldName="QuizzChoices"><![CDATA[le cochon d'inde;l'autruche;le crocodile]]></FieldValue>
  71:                        <FieldValue FieldName="QuizzAnswers"><![CDATA[2]]></FieldValue>
  72:                        <FieldValue FieldName="QuizzCategory"><![CDATA[3;#Reptile]]></FieldValue>
  73:                      </FieldsValues>
  74:                    </ListItem>
  75:                  </ListItems>
  76:                </List>
  77:              </Lists>
  78:            </Web>
  79:          </Site>
  80:        </Sites>
  81:      </WebApp>
  82:    </WebApps>
  83:  </Farm>

L'avantage de cet outil est qu'il cumule à la fois la création d'un environnement (qui aurait pu être scripté via PowerShell ou STSADM) et l'ajout de contenu SharePoint (GED, WCM, Contenu personnalisé).

De plus dans le cadre de la série de post liée au Quizz, il me permet de vous décrire simplement le contenu (grâce au fichier XML) sur lequel je vais m'appuyer dans la suite des développements

Ainsi après lancement, voilà ce que l'on obtient :

  • Une WebApp créée sur le port 80 avec une SiteCollection sur "/" nommée Intranet

    image 

  • La feature Quizz déjà activée

    image

  • Une liste catégorie créée et pré-remplie

    image

  • Une liste Quizz Animaux créée en utlisant la définition adéquate et pré-remplie

      image 

Le tout en 2-3 minutes, juste le temps qu'il faut pour se préparer un bon café :).

Voilà, je ne vais pas m'attarder sur cet outil, j'en parlerais plus à l'occasion de la session que je co-presente avec Adrien au Techdays, je voulais simplement vous montrer comment je crée mon environnement SharePoint pour chacun des projets sur lequel je travaille.

Dans le prochain post, on abordera toute les écrans nécessaire au bon fonctionnement de l'application Quizz.

<Philippe/>

SharePoint 2007 : Définition de liste (ListDefinition) et type de contenu (ContentTypeRef)

Dans le précédent post, je vous ai montré comment créé une définition de liste et lier un ContentType à cette liste de la façon suivante :

QuizzListDefinition > schema.xml

   1:  <?xml version="1.0" encoding="utf-8"?>
   2:  <List Title="QuizzListDefinition" FolderCreation="FALSE" 
   3:        Direction="$Resources:Direction;" Url="Lists/Quizz List" BaseType="0">
   4:    <MetaData>
   5:      <ContentTypes>
   6:        <ContentTypeRef ID="0x010081DA7661E43E254B8B4F823AA9A3F578"/>
   7:        <ContentTypeRef ID="0x0120" />
   8:      </ContentTypes>
   9:      <Fields>
  10:        <!-- Copier coller des champs QuestionMenu et NoMenu  -->
  11:      </Fields>
  12:      <Views>
  13:        <View BaseViewId="0" Type="HTML"><!-- ... --></View>
  14:        <View BaseViewID="1" Type="HTML" WebPartZoneID="Main" 
  15:              DisplayName="$Resources:core,objectiv_schema_mwsidcamlidC24;" 
  16:              DefaultView="TRUE" SetupPath="pages\viewpage.aspx" 
  17:              ImageUrl="/_layouts/images/generic.png" Url="AllItems.aspx">
  18:          <!-- ... -->
  19:          <ViewFields>
  20:            <FieldRef Name="QuizzQuestionMenu" />
  21:            <FieldRef Name="QuizzChoices" />
  22:            <FieldRef Name="QuizzAnswers" />
  23:            <FieldRef Name="QuizzCategory" />
  24:          </ViewFields>
  25:          <Query><!-- ... --></Query>
  26:        </View>
  27:      </Views>
  28:      <Forms><!-- ... --></Forms>
  29:    </MetaData>
  30:  </List>

Comme je disais précédemment, Le fait de réferencer le ContentType (<ContentTypeRef ID="0x010081DA7661E43E254B8B4F823AA9A3F578"/>) n'est pas suffisant pour faire le lien entre le ContentType et la liste.

Pour que cela fonctionne, il faudrait mettre en plus les colonnes de site du ContentType dans les champs de la liste... Bref dupliquer la définition du ContentType. Pas terrible...

Haut les coeurs, il existe une solution plus élégante qui permet de faire fonctionner la definition de liste ci-dessus.

Cette solution provient d'un dénommé Martin Hatch dans son billet sur [Code Update] SDK and Microsoft Press Both Wrong?? Custom Fields in the schema.xml.

Pour faire simple. Martin s'est rendu compte qu'il est possible de ne pas avoir à dupliquer les informations du ContentType en exécutant la manipulation suivante :

  1. Création d'un ContentType
  2. Création d'une définition de liste
  3. Ajout du ContentTypeRef dans la définition de la liste
  4. Activation de la feature définissant le ContentType
  5. Accè à la gallerie de ContentType et modification du ContentType (modifier un champ, supprimer, renommer, qu'importe)
  6. Activation de la feature de la définition de liste
  7. Création de la liste et verifier que le ContentType est bien référéncer et que les colonnes de site sont présentes

Assez étrange, comme comportement me direz vous, mais en tout cas ça fonctionne :)

Maintenant, il faut pouvoir automatiser tout ça. La solution donnée par Martin est de rajouter un FeatureReceiver à la feature du ContentType, quequechose dans ce style :

ContentTypeQuizz > feature.xml

   1:  <?xml version="1.0"?>
   2:  <Feature xmlns="http://schemas.microsoft.com/sharepoint/"
   3:           Id="08BF6137-EDD2-4401-A536-2A37766D49AC"
   4:           Title="$Resources:FeatureTitle;"
   5:           Description="$Resources:FeatureDescription;"
   6:           Scope="Site"
   7:           Hidden="True"
   8:           DefaultResourceFile="_Res"
   9:           ReceiverAssembly="SharePointOfView, Version=1.1.0.0, Culture=neutral, PublicKeyToken=85a56337686e234b"
  10:           ReceiverClass="SharePointOfView.EventReceivers.ContentTypeUpdaterFeatureReceiver"
  11:           Version="1.0.0.0">
  12:    <ElementManifests>
  13:      <ElementManifest Location="elements.xml" />
  14:    </ElementManifests>
  15:   
  16:  </Feature>

Le fichier elements.xml est un fichier classique de définition de ContentType.

ContentTypeQuizz > elements.xml

   1:  <?xml version="1.0"?>
   2:  <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
   3:    <ContentType ID="0x010081DA7661E43E254B8B4F823AA9A3F578" Name="QuizzItem" Group="Demo" Version="12" xmlns="http://schemas.microsoft.com/sharepoint/" Hidden="False" ReadOnly="False" Sealed="False">
   4:      <FieldRefs>
   5:        <FieldRef ID="{3bfd9f63-3c57-4a0d-b22a-3ed324150348}" Name="$Resources:Quizz,Quizz_Field_Question;" Required="TRUE" Hidden="FALSE" ReadOnly="FALSE" />
   6:        <FieldRef ID="{48b006bf-5e14-43cb-b5cb-9a3dae4f0646}" Name="$Resources:Quizz,Quizz_Field_Category;" Required="TRUE" Hidden="FALSE" ReadOnly="FALSE" />
   7:        <FieldRef ID="{71d78a3d-eaad-4e6e-b905-e183cf774693}" Name="$Resources:Quizz,Quizz_Field_Choices;" Required="TRUE" Hidden="FALSE" ReadOnly="FALSE" />
   8:        <FieldRef ID="{2a3f841f-ff0a-4348-9028-cf5e0a16a089}" Name="$Resources:Quizz,Quizz_Field_Answers;" Required="TRUE" Hidden="FALSE" ReadOnly="FALSE" />
   9:        <RemoveFieldRef ID="{fa564e0f-0c70-4ab9-b863-0177e6ddd247}" Name="Title" />
  10:      </FieldRefs>
  11:    </ContentType>
  12:  </Elements>

Si vous suivez mes posts, vous aurez compris que j'ai tendance à mettre tout ce qui peut être réutilisable dans notre projet Codeplex : SharePointOfView. Du coup, vous pourrez d'ailleurs retrouver dans les derniers check-ins, le FeatureReceiver "ContentTypeUpdaterFeatureReceiver" (Le code de ce FeatureReceiver est la propriété de Martin Hatch comme précisé dans la description de la fonction)

Nous avons maintenant fait le tour de toute la plomberie nécéssaire au fonctionnement de l'application. Dans le prochain post, je vous parlerai d'un outil développé par Adrien Siffermann et moi-même qui permet de déployer des WSP, de créer du "contenu SharePoint" (WebApplication, SiteCollection, Site, List, Items, WebPart, etc.) et pleins d'autre fonctionnalités...

J'utiliserai cet outil pour créer mon environnement SharePoint pour ce projet :

  1. Upload de la Solution
  2. Création de la WebApplication
  3. Déploiement de la Solution
  4. Création de la SiteCollection
  5. Activation des Features
  6. Création d'une liste Quizz Animaux basé sur notre ListDefinition
  7. Ajout de catégories dans la liste Quizz Catégories
  8. Ajout de Questions à la liste Quizz Animaux.

Le tout en une ligne de commande : SharePointDataPopulate.Client.exe" -filename "Demo.SharePoint.Quizz.Orijin.Local.xml" -verbose.

PS : On cherche encore un nom pour cet outil :)

<Philippe/>


Les 10 derniers blogs postés

- Office 365: Script PowerShell pour auditer l’usage des Office Groups de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 11:02

- Office 365: Script PowerShell pour auditer l’usage de Microsoft Teams de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 10:39

- Office 365: Script PowerShell pour auditer l’usage de OneDrive for Business de votre tenant par Blog Technique de Romelard Fabrice le 04-25-2019, 15:13

- Office 365: Script PowerShell pour auditer l’usage de SharePoint Online de votre tenant par Blog Technique de Romelard Fabrice le 02-27-2019, 13:39

- Office 365: Script PowerShell pour auditer l’usage d’Exchange Online de votre tenant par Blog Technique de Romelard Fabrice le 02-25-2019, 15:07

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Stream Portal par Blog Technique de Romelard Fabrice le 02-21-2019, 17:56

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Video Portal par Blog Technique de Romelard Fabrice le 02-18-2019, 18:56

- Office 365: Script PowerShell pour extraire les Audit Log basés sur des filtres fournis par Blog Technique de Romelard Fabrice le 01-28-2019, 16:13

- SharePoint Online: Script PowerShell pour désactiver l’Option IRM des sites SPO non autorisés par Blog Technique de Romelard Fabrice le 12-14-2018, 13:01

- SharePoint Online: Script PowerShell pour supprimer une colonne dans tous les sites d’une collection par Blog Technique de Romelard Fabrice le 11-27-2018, 18:01