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/>

SharePoint 2007 : Liste Personnalisée, Colonnes de Site et Menu Contextuel

Dans le précédent post, je vous avais montré à quoi ressemblait la liste de Quizz que j'avais crée :

image

Or si vous regardez bien le champ question, il est un peu particulier...c'est loin d'être un champ Titre classique (le contenu est en italique et en gras)

Mais avant d'expliquer en détail comment obtenir ce genre de champ, voyons ce qui a été précédemment réalisé (cf post précédents, ici et ) :

  1. Création d'une feature QuizzFields qui définit 4 champs : Réponse, Choix, Catégorie et Question.
  2. Création d'une feature QuizzContentType qui définit le type de contenu regroupant ces 4 champs

Seulement je ne vous ai pas tout montré lors du dernier post, en effet le fichier elements.xml correspondant à QuizzField définit des champs supplémentaires :

QuizzFields > elements.xml

   1:  <?xml version="1.0"?>
   2:  <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
   3:    ...
   4:    <Field ID="{7BCDA1EC-2484-4703-8288-4990D03551E6}"
   5:      Name="QuizzQuestionNoMenu"
   6:      SourceID="http://schemas.microsoft.com/sharepoint/v3"
   7:      StaticName="QuizzQuestionNoMenu"
   8:      Group="Demo"
   9:      ReadOnly="TRUE"
  10:      Type="Computed"
  11:      DisplayName="$Resources:Quizz,Quizz_Field_Question;"
  12:      Dir=""
  13:      DisplayNameSrcField="QuizzQuestion"
  14:      AuthoringInfo="$Resources:core,Linked_Item;">
  15:      <FieldRefs>
  16:        <FieldRef ID="{3bfd9f63-3c57-4a0d-b22a-3ed324150348}" Name="QuizzQuestion"/>
  17:      </FieldRefs>
  18:      <DisplayPattern>
  19:        <IfNew>
  20:          <HTML><![CDATA[<IMG Style='float:right;' SRC="/_layouts/[%=System.Threading.Thread.CurrentThread.CurrentUICulture.LCID%]/images/new.gif" alt="]]></HTML>
  21:          <HTML>$Resources:core,new_gif_alttext</HTML>
  22:          <HTML><![CDATA[">]]></HTML>
  23:        </IfNew>
  24:        <HTML><![CDATA[<a onfocus="OnLink(this)" href="]]></HTML>
  25:        <URL/>
  26:        <HTML><![CDATA[" ONCLICK="GoToLink(this);return false;" target="_self">]]></HTML>
  27:        <Column Name="QuizzQuestion" Default="$Resources:core,NoTitle"/>
  28:        <HTML><![CDATA[</a>]]></HTML>
  29:      </DisplayPattern>
  30:    </Field>
  31:    <Field ID="{CAC4431D-FBFD-41A0-9F3A-C1FBC2FDB223}"
  32:        Name="QuizzQuestionMenu"
  33:        SourceID="http://schemas.microsoft.com/sharepoint/v3"
  34:        StaticName="QuizzQuestionMenu"
  35:        Group="Demo"
  36:        ReadOnly="TRUE"
  37:        Type="Computed"
  38:        DisplayName="$Resources:Quizz,Quizz_Field_Question;"
  39:        DisplayNameSrcField="QuizzQuestion"
  40:        ClassInfo="Menu"
  41:        AuthoringInfo="$Resources:core,Linked_Item_With_Menu;">
  42:      <FieldRefs>
  43:        <FieldRef ID="{3bfd9f63-3c57-4a0d-b22a-3ed324150348}" Name="QuizzQuestion"/>
  44:        <FieldRef ID="{7BCDA1EC-2484-4703-8288-4990D03551E6}" Name="QuizzQuestionNoMenu"/>
  45:        <FieldRef ID="{3c6303be-e21f-4366-80d7-d6d0a3b22c7a}" Name="_EditMenuTableStart"/>
  46:        <FieldRef ID="{2ea78cef-1bf9-4019-960a-02c41636cb47}" Name="_EditMenuTableEnd"/>
  47:      </FieldRefs>
  48:      <DisplayPattern>
  49:        <FieldSwitch>
  50:          <Expr>
  51:            <GetVar Name="FreeForm"/>
  52:          </Expr>
  53:          <Case Value="TRUE">
  54:            <Field Name="QuizzQuestionNoMenu"/>
  55:          </Case>
  56:          <Default>
  57:            <Field Name="_EditMenuTableStart"/>
  58:            <Field Name="QuizzQuestionNoMenu"/>
  59:            <Field Name="_EditMenuTableEnd"/>
  60:          </Default>
  61:        </FieldSwitch>
  62:      </DisplayPattern>
  63:    </Field>
  64:  </Elements>

Ce qui revient à dire tout simplement à SharePoint de créer 2 champs liés à QuizzQuestion :

  • QuizzQuestionNoMenu affichera la question avec un lien vers la page d'information
  • QuizzQuestionMenu affichera QuizzQuestionNoMenu mais avec un menu contextuel

image

Vous l'aurez compris, dans l'exemple ci dessus, il s'agit simplement d'englober le champ qui nous interesse par EditMenuTableStart et EditMenuTableEnd.

Une fois les champs définis, il ne me reste plus qu'à créer ma définition de liste de la façon suivante :

Créons une définition basé sur une liste personnalisé (copier-coller basique provenant du répertoire C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\TEMPLATE\FEATURES\CustomList)

  • Préciser que cette liste utilisera le ContentType Quizz
  • Rajouter les champs d'affichage (QuizzQuestionMenu et QuizzQuestionNoMenu) dans la section <Fields>
  • Rajouter les champs désirés (QuizzQuestionMenu, QuizzChoices, QuizzAnswers, QuizzCategory) dans la section <Views><View><ViewFields>

Et le tour est joué ! enfin c'est vite dit, voilà le bout d'XML tronqué (sinon le fichier fait dans les 4000 lignes):

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>


QuizzListDefinition > elements.xml

   1:  <?xml version="1.0" encoding="utf-8"?>
   2:  <Elements Id="ac51712b-36a6-4afc-bf95-efee98b8ddb0" xmlns="http://schemas.microsoft.com/sharepoint/">
   3:    <ListTemplate
   4:        Name="QuizzListDefinition"
   5:        DisplayName="$Resources:Quizz,Quizz_List_Definition_DisplayName;"
   6:        Type="10042"
   7:        Description="$Resources:Quizz,Quizz_List_Definition_Description;"
   8:        BaseType="0"
   9:        OnQuickLaunch="TRUE"
  10:        SecurityBits="11" />
  11:  </Elements> 


QuizzListDefinition > feature.xml

   1:  <Feature xmlns="http://schemas.microsoft.com/sharepoint/"
   2:           Id="65E774A0-0B40-4341-B694-429B61A6EE15"
   3:           Title="$Resources:FeatureTitle;"
   4:           Description="$Resources:FeatureDescription;"
   5:           Scope="Site"
   6:           Hidden="True"
   7:           DefaultResourceFile="_Res"
   8:           Version="1.0.0.0">
   9:    <ElementManifests>
  10:      <ElementManifest Location="elements.xml" />
  11:    </ElementManifests>
  12:  </Feature>

Il ne reste plus qu'à activer la feature et tout celles dont elle dépend et vous pourrez créer votre ... ou presque.

En effet, les habitués des definitions de liste savent très bien que la déclaration du fichier schema.xml ne fonctionnera pas. Le fait de réferencer le ContentType (<ContentTypeRef ID="0x010081DA7661E43E254B8B4F823AA9A3F578"/>) n'est pas suffisant pour faire le lien entre le ContentType et la liste, il faudrait mettre en plus les colonnes de site du ContentType dans les champs de la liste... les dupliquer en quelque sorte...

Et bien, non. Il existe une solution ! J'en parlerai dans le prochain post :)

<Philippe/>

SharePoint 2007 : Les champs Lookup et SharePointOfView

Si vous ne connaissez pas encore notre projet Codeplex "SharePointOfView : http://blogs.developpeur.org/phil/archive/2008/06/30/sharepoint-2007-projet-codeplex-sharepointofview.aspx

Dans le dernier post, je vous ai expliqué comment créé simplement des colonnes de site en exportant les informations depuis SharePoint grâce à une application très intuitive : Imtech FieldsExplorer.

Néanmoins, il y a un petit souci lors de l'export sur les champs Lookup.

   0:  
   1:  <Field Type="Lookup" DisplayName="Catégorie" Required="FALSE" List="b23273d2-946b-41e8-81cb-8911dadc2f92" ShowField="Title" Sortable="FALSE" UnlimitedLengthInDocumentLibrary="FALSE" ID="{48b006bf-5e14-43cb-b5cb-9a3dae4f0646}" Group="Demo" StaticName="QuizzCategory" Name="QuizzCategory" SourceID="http://schemas.microsoft.com/sharepoint/v3" xmlns="http://schemas.microsoft.com/sharepoint/" WebId="1483aa7c-a45b-410e-977c-45174ccbce5d" Version="1" />
   2:  

Ce bout d'XML précise à SharePoint que le champ Catégorie recupère la valeur "Title" de chaque éléments de la liste qui est référencé par le Guid : b23273d2-946b-41e8-81cb-8911dadc2f92 dans le Web référencé par le Guid : 1483aa7c-a45b-410e-977c-45174ccbce5d de la collection courante....

Or comme ces GUIDs sont différents entre chaque environnement SharePoint (celle des developpeurs, celle d'intégration, recette, prod), il n'est pas possible de se baser sur ce type d'information.

Si vous commencez à vous creuser la tête et à fouiller la toile à la recherche d'une solution (http://delicious.com/philippesentenac/lookup), vous trouverez qu'un certain Chris O'Brien a utilisé la solution suivante :

Placer dans un fichier XML, le texte suivant qui ressemble enormement à une déclaration de Field standard si ce n'est le List="Lists/QuizzAnimaux".

   1:  <?xml version="1.0"?>
   2:  <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
   3:    <Field Type="Lookup" DisplayName="$Resources:Quizz,Quizz_Field_Category;" Required="FALSE"
   4:           List="Lists/QuizzAnimaux" ShowField="Title" Sortable="FALSE" UnlimitedLengthInDocumentLibrary="FALSE"
   5:           ID="{48b006bf-5e14-43cb-b5cb-9a3dae4f0646}" Group="Demo"
   6:           StaticName="QuizzCategory" Name="QuizzCategory" SourceID="http://schemas.microsoft.com/sharepoint/v3"/>
   7:  </Elements> 

L'idée est de récupérer le GUID correspondant à cette liste et de remplacer la valeur de l'attribut List lors de l'activation d'une Feature via un FeatureReceiver.

Voyons en détails :

Après export des fields avec l'outil cité précédemment, j'organise la Feature "QuizzFields" comme ci-dessous.

FeatureQuizzFields

feature.xml

   1:  <?xml version="1.0"?>
   2:  <Feature xmlns="http://schemas.microsoft.com/sharepoint/"
   3:           Id="BA0AD5C7-C8A4-413C-8DE2-578634474470"
   4:           Title="$Resources:FeatureTitle;"
   5:           Description="$Resources:FeatureTitle;"
   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.LookupCreationFeatureReceiver"
  11:           Version="1.0.0.0">
  12:    <ElementManifests>
  13:      <ElementManifest Location="elements.xml" />
  14:    </ElementManifests>
  15:    <Properties>
  16:      <Property Key="lookupFile" Value="lookupFields.xml"/>
  17:      <Property Key="resourceFile" Value="Quizz"/>
  18:    </Properties>
  19:  </Feature> 

Vous remarquerez l'utilisation d'un nouveau FeatureReceiver de SharePoint : le LookupCreationFeatureReceiver.

elements.xml

   1:  <?xml version="1.0"?>
   2:  <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
   3:    <Field Type="Note" DisplayName="$Resources:Quizz,Quizz_Field_Answers;" Required="FALSE" NumLines="6"
   4:           RichText="TRUE" RichTextMode="Compatible" Sortable="FALSE" Group="Demo" ID="{2a3f841f-ff0a-4348-9028-cf5e0a16a089}"
   5:            StaticName="QuizzAnswers" Name="QuizzAnswers" SourceID="http://schemas.microsoft.com/sharepoint/v3"/>
   6:    <Field Type="Note" DisplayName="$Resources:Quizz,Quizz_Field_Choices;" Required="FALSE" NumLines="6"
   7:           RichText="TRUE" RichTextMode="Compatible" Sortable="FALSE" Group="Demo" ID="{71d78a3d-eaad-4e6e-b905-e183cf774693}"
   8:            StaticName="QuizzChoices" Name="QuizzChoices" SourceID="http://schemas.microsoft.com/sharepoint/v3"/>
   9:    <Field Type="Note" DisplayName="$Resources:Quizz,Quizz_Field_Question;" Required="FALSE" NumLines="6"
  10:           RichText="TRUE" RichTextMode="Compatible" Sortable="FALSE" Group="Demo" ID="{3bfd9f63-3c57-4a0d-b22a-3ed324150348}"
  11:            StaticName="QuizzQuestion" Name="QuizzQuestion"/>
  12:    <Field ... />
  13:    <Field ... />
  14:  </Elements>

Je reviendrai dans un précédent post sur les deux autres champs qui ne sont pas définis dans cet extrait...

lookupFields.xml

   1:  <?xml version="1.0"?>
   2:  <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
   3:    <Field Type="Lookup" DisplayName="$Resources:Quizz,Quizz_Field_Category;" Required="FALSE"
   4:           List="$Resources:Quizz,Quizz_List_QuizzCategory_Title;" ShowField="Title" Sortable="FALSE" UnlimitedLengthInDocumentLibrary="FALSE"
   5:           ID="{48b006bf-5e14-43cb-b5cb-9a3dae4f0646}" Group="Demo"
   6:           StaticName="QuizzCategory" Name="QuizzCategory" SourceID="http://schemas.microsoft.com/sharepoint/v3"/>
   7:  </Elements>


SharePointOfView.EventReceivers.LookupCreationFeatureReceiver

   1:  namespace SharePointOfView.EventReceivers
   2:  {
   3:      public class LookupCreationFeatureReceiver : SPFeatureReceiver
   4:      {
   5:          /// <summary>
   6:          /// Occurs after a Feature is activated.
   7:          /// Get values for properties named 'lookupFile' and 'resourceFile'. The key 'lookupfile' is mandatory
   8:          /// This code is based on the solution provided by Chris O'Brien at http://www.codeplex.com/SP2007LookupFields
   9:          /// </summary>
  10:          /// <param name="properties">An <see cref="T:Microsoft.SharePoint.SPFeatureReceiverProperties"></see> object that represents the properties of the event.</param>
  11:          public override void FeatureActivated(SPFeatureReceiverProperties properties)
  12:          {
  13:              SPSite site = properties.Feature.Parent as SPSite;
  14:              SPWeb web = site.RootWeb; 
  15:   
  16:              try
  17:              {
  18:                  string filePath = Path.Combine(properties.Definition.RootDirectory, properties.Feature.Properties["lookupFile"].Value);
  19:                  string resourceFile = string.Empty; 
  20:   
  21:                  try
  22:                  {
  23:                      resourceFile = properties.Feature.Properties["resourceFile"].Value;
  24:                  }
  25:                  catch { } 
  26:   
  27:                  XmlDocument xmlDoc = new XmlDocument();
  28:                  xmlDoc.Load(filePath); 
  29:   
  30:                  XmlNamespaceManager nsMgr = new XmlNamespaceManager(xmlDoc.NameTable);
  31:                  nsMgr.AddNamespace("def", "http://schemas.microsoft.com/sharepoint/"); 
  32:   
  33:                  XmlNodeList fields = xmlDoc.SelectNodes(".//def:Field", nsMgr); 
  34:   
  35:                  foreach (XmlNode field in fields)
  36:                  {
  37:                      string listName = string.Empty; 
  38:   
  39:                      foreach (XmlAttribute attribute in field.Attributes)
  40:                      {
  41:                          if (attribute.Value.StartsWith("$Resources:") && !string.IsNullOrEmpty(resourceFile))
  42:                          {
  43:                              attribute.Value = Localization.GetResource(attribute.Value.Replace("$Resources:", string.Empty), resourceFile, web.Language);
  44:                          } 
  45:   
  46:                          if (attribute.Name == "List")
  47:                          {
  48:                              listName = attribute.Value;
  49:                          }
  50:                      } 
  51:   
  52:                      SPList referencedList; 
  53:   
  54:                      if (listName != String.Empty && web.Lists.SovTryGet(listName, out referencedList))
  55:                      {
  56:                          field.Attributes["List"].Value = referencedList.ID.ToString();
  57:                          string realFieldToCreate = field.OuterXml; 
  58:   
  59:                          string lookupColumnName = web.Fields.AddFieldAsXml(realFieldToCreate); 
  60:   
  61:                          SPFieldLookup lookupColumn = web.Fields.GetFieldByInternalName(lookupColumnName) as SPFieldLookup; 
  62:   
  63:                          lookupColumn.LookupWebId = web.ID;
  64:                          lookupColumn.Update();
  65:                      }
  66:                  }
  67:              }
  68:              catch (Exception ex)
  69:              {
  70:                  ULS.WriteError("Error activating lookup creation feature. Message : " + ex.Message, "SharePointOfView");
  71:                  site.Features.Remove(properties.Feature.DefinitionId);
  72:                  throw ex;
  73:              }
  74:              finally
  75:              {
  76:                  web.Dispose();
  77:              }
  78:          } 
  79:   
  80:          /// <summary>
  81:          /// Occurs when a Feature is deactivated.
  82:          /// </summary>
  83:          /// <param name="properties">An <see cref="T:Microsoft.SharePoint.SPFeatureReceiverProperties"></see> object that represents the properties of the event.</param>
  84:          public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
  85:          {
  86:              SPSite site = properties.Feature.Parent as SPSite;
  87:              SPWeb web = site.RootWeb; 
  88:   
  89:              try
  90:              {
  91:                  string filePath = Path.Combine(properties.Definition.RootDirectory, properties.Feature.Properties["lookupFile"].Value); 
  92:   
  93:                  XmlDocument xmlDoc = new XmlDocument();
  94:                  xmlDoc.Load(filePath); 
  95:   
  96:                  XmlNamespaceManager nsMgr = new XmlNamespaceManager(xmlDoc.NameTable);
  97:                  nsMgr.AddNamespace("def", "http://schemas.microsoft.com/sharepoint/"); 
  98:   
  99:                  XmlNodeList fields = xmlDoc.SelectNodes(".//def:Field/@ID", nsMgr); 
 100:   
 101:                  Guid lookupfieldGuid;
 102:                  foreach (XmlNode field in fields)
 103:                  {
 104:                      lookupfieldGuid = new Guid(field.Value); 
 105:   
 106:                      SPFieldLookup lookupColumn = web.Fields[lookupfieldGuid] as SPFieldLookup;
 107:                      lookupColumn.Delete();
 108:                  }
 109:              }
 110:              catch (Exception ex)
 111:              {
 112:                  ULS.WriteInformation("Error de-activating lookup creation feature. Message : " + ex.Message, "SharePointOfView");
 113:              } 
 114:   
 115:              web.Dispose();
 116:          } 
 117:   
 118:          #region Not Used
 119:          public override void FeatureInstalled(SPFeatureReceiverProperties properties)        {         } 
 120:          public override void FeatureUninstalling(SPFeatureReceiverProperties properties)        {         }
 121:          #endregion
 122:      }
 123:  } 
 124:   

Ce bout de code se résume ainsi :

  1. On récupère le chemin complet vers le fichier spécifié dans la propriété lookupFile et eventuellement les fichier ressource
  2. Pour chaque champ "Field" spécifié dans le fichier, on traduit les valeurs des attributs commencant par $Resources et on récupère l'url de la liste.
  3. Si la liste existe dans le site alors on fait le remplacement de l'url en Guid et on crée le Field

La désactivation fait grosso modo la même chose mais supprime le champ au lieu de le créer :)

Vous pourrez retrouver ce FeatureReceiver dans les derniers bouts de code archivés dans le projet codeplex SharePointOfView : http://codeplex.com/SharePointOfView

Je n'ai pas encore créé de nouvelle release mais ca ne saurait tarder :)

Dans le prochain post, je vous expliquerais comment obtenir rajouter dans proprement dans une liste, un menu sur une colonne de site personnalisé :

CustomFieldMenu

A la semaine prochaine :)

<Philippe/>

SharePoint 2007 : Création de Fields et ContentTypes, vite fait, bien fait.

Après le précédent post sur l'activation de features et les ActivationDependencies, je voulais vous parler d'un manière très personnelle de créer des Fields et ContentTypes (en français : colonnes de sites et types de contenu).

Comme vous le savez sans doute, il existe plusieurs possibilités pour créer ce genre de composants SharePoint :

  • Via l'interface sharepoint : Facile à faire mais a l'inconvénient de devoir être fait sur l'environnement de destination, ce qui requiert des manipulations manuelle et chronophage.
  • Via le modèle objet en utilisant des FeatureReceiver : On se retrouve dans l'environnement de prédilection du développeur mais requiert beaucoup de plomberie pour pas grand chose au final.
  • Via les fichier XML : Facilement packageable mais sujet à des erreurs de typos.

Souvent dans le contexte du développement d'application SharePoint et du packaging qui va avec, on va avoir tendance à préférer les deux dernières options en choisissant en fonction de votre préférence.

Pour ma part j'utilise un mix des trois :). J'espère vous convaincre de l'intérêt de ma méthode.

Création des Fields et ContentType via l'interface SharePoint

Tout commence sur votre VPC SharePoint en accèdant à l'interface d'administation :

image

Il vous suffit de créer les champs qui vous intéressent, en précisant vos configuration pour chaque champs :

GestionFields

Dans mon cas, j'ai crée 4 champs : Catégorie, Choix, Question, Réponses. Une petite précision cependanta, Catégorie est un champ de recherche pointant sur le titre des éléments contenus dans une liste Catégorie crée précédemment.

Je n'ai plus qu'à créé mon type de contenu en configurant l'ordre de mes champs, leur état (obligatoire, optionnel, masqué), etc... :

GallerieContentType

Colonnes

C'est vrai que c'est vraiment rapide avec l'interface SharePoint, peut être 10 mn au maximum alors qu'il m'en aurait fallu bien plus avec les deux autres options

Export des Fields et ContentTypes

Le problème est que je souhaiterais packager le tout dans une ou plusieurs feature, "histoire de faire les choses proprement" et de les intégrer à ma Solution (fichier wsp).

Pour ce faire, je vais utiliser quelques outils/projets développée par la communauté qui vont me permettre d'exporter tout ce que je vient de faire sous la forme de plusieurs fichiers XML.

L'outil en question se nomme Fields Explorer : http://www.codeplex.com/tmt/Release/ProjectReleases.aspx?ReleaseId=17330 et vous pourrez le trouvez sur Codeplex comme d'habitude :)

PS : Pour ceux qui se demandent pourquoi j'ai plusieurs champs Question lorsque je fais un export, je vous raconterais ça dans un autre post.

Une fois tout ces fichiers XML récupérés, il ne me reste plus qu'à organiser un peu tout ça, retravailler rapidement les fichiers pour y intégrer l'utilisation de ressources, mettre des noms internaux "pratiques" (sans espaces), etc.. Quelque chose qui ressemblerait à ça dans votre Visual Studio.

SolutionVisualStudio

Il ne nous reste plus qu'à packager ça gràce à WSPBuilder (http://codeplex.com/wspbuilder) et le tour est jouer :).

Néanmoins, il nous reste un petit problème... la gestion du champ Catégorie.

En effet, il n'existe pas de moyen simple dans ce contexte de déclarer un champ de Recherche (lookup) en XML. Celui généré par l'outil (qui ne fait qu'exporter la représentation du champ dans SharePoint) ressemble à ça :

   0:  
   1:  <Field Type="Lookup" DisplayName="Catégorie" Required="FALSE" List="b23273d2-946b-41e8-81cb-8911dadc2f92" ShowField="Title" Sortable="FALSE" UnlimitedLengthInDocumentLibrary="FALSE" ID="{48b006bf-5e14-43cb-b5cb-9a3dae4f0646}" Group="Demo" StaticName="QuizzCategory" Name="QuizzCategory" SourceID="http://schemas.microsoft.com/sharepoint/v3" xmlns="http://schemas.microsoft.com/sharepoint/" WebId="1483aa7c-a45b-410e-977c-45174ccbce5d" Version="1" />
   2:  

En traduction pour les non SharePointeurs, ce bout d'XML précise à SharePoint que le champ Catégorie recupère la valeur "Title" de chaque éléments de la liste qui est référencé par le Guid : b23273d2-946b-41e8-81cb-8911dadc2f92 dans le Web référencé par le Guid : 1483aa7c-a45b-410e-977c-45174ccbce5d de la collection courante....

Bref pas très lisible et surtout pas très utilisable quand on sait que lorsqu'on passera sur d'autres environnement (d'intégration, recette et production), le même site SharePoint (même URL, Nom, etc...) sera référencé par un nouveau Guid et c'est la même chose pour les listes.

Bref, comment faire ? Et bien, on verra ça dans le prochain post :)

<Phil/>

SharePoint 2007 : Activation de Features et ActivationDependencies

Une rapide recherche sur ActivationDependencies vous permettra de comprendre que c'est un élément du schéma assigné aux features qui vous permet :

  • Activer un sous ensemble de features (si elles ont l'attribut Hidden == True)
  • Empecher l'activation d'une feature si une autre n'est pas activée (faire dépendre une feature de scope Web, d'une feature de scope Site par exemple)

Je ne vais pas insister sur cette partie là qui a déjà été revue maintes et maintes fois sur plusieurs blogs mais si vous voulez plus d'informations, voici quelques liens :

Grosso modo, ca ressemble à ça :

   1:  <Feature xmlns="http://schemas.microsoft.com/sharepoint/"
   2:           Id="349C60C9-1B08-4C66-BDF2-ACCBBC46D9FF"
   3:           Title="$Resources:FeatureTitle;"
   4:           Description="$Resources:FeatureDescription;"
   5:           Scope="Site"
   6:           Hidden="False"
   7:           DefaultResourceFile="_Res"
   8:           Version="1.0.0.0">
   9:    <ActivationDependencies>
  10:      <ActivationDependency FeatureId="FBC83A15-59B6-4F52-9863-893A3BBA63AD"/>
  11:      <ActivationDependency FeatureId="BA0AD5C7-C8A4-413C-8DE2-578634474470"/>
  12:      <ActivationDependency FeatureId="08BF6137-EDD2-4401-A536-2A37766D49AC"/>
  13:      <ActivationDependency FeatureId="36541092-DC4F-42AC-820D-0B82E6A45E6B"/>
  14:    </ActivationDependencies>
  15:  </Feature>

Bien que ce mécanisme puisse se révêler être très interessant dans certains cas, je me retrouve de plus en plus souvent à ne pas l'utiliser pour une raison toute simple : Je souhaite activer mes features dans une ordre donné et les désactiver dans l'ordre inverse...

Par exemple, je me retrouve dans ce cas là, lorsque j'ai besoin de créé une liste "Catégorie" dont dépendra un Champ de type "Lookup" que j'intègre à un ContentType "Quizz" qui sera lui même utilisé dans une autre liste (Oui, je sais ca à l'air compliqué comme ça). Pour que tout fonctionne, il faut, bien sur, que je crée tout dans un certain ordre lors de l'activation : Liste Categorie > Lookup > ContentType > Liste Quizz.

Mais lors de la désactivation, c'est l'inverse : Suppréssion de la liste Quizz > ContentType > Lookup > Liste Catégorie.

Ce qui m'oblige à utiliser mon propre système...

Voyons un peu comment j'ai fait :

image La Feature Quizz est la feature qui englobera l'ensemble des autres Features que j'ai décidé de cacher. Elle activera à tour de rôle QuizzListCategory, QuizzFields, QuizzContentType et QuizzListAnimals.

QuizzFields est un peu particulière car elle va à la fois créee des champs "classiques" et un champs de type Lookup, je m'attarderai sur cette fonctionnalité dans un autre post.

QuizzListAnimals et QuizzListCategory ne sont rien d'autre que des fichier XML faisant appel à des FeatureReceiver qui créeront les listes et les paramètreront.

QuizzContentType est très classique et défini le contentType via un fichier XML.

Finalement, voyons en détails la feature Quizz...

Quizz > Feature.xml

   1:  <?xml version="1.0" encoding="utf-8" ?>
   2:  <Feature xmlns="http://schemas.microsoft.com/sharepoint/"
   3:           Id="321BBA1F-47A9-4B0F-BB17-3A34CFD58CCE"
   4:           Title="$Resources:FeatureTitle;"
   5:           Description="$Resources:FeatureDescription;"
   6:           Scope="Site"
   7:           Hidden="False"
   8:           DefaultResourceFile="_Res"
   9:           ReceiverAssembly="Demo.SharePoint.Quizz.Global, Version=1.0.0.0, Culture=neutral, PublicKeyToken=36d82c640787533c"
  10:           ReceiverClass="Demo.SharePoint.Quizz.Global.FeatureReceiver.QuizzFeatureReceiver"
  11:           Version="1.0.0.0">
  12:    <Properties>
  13:      <Property Key="QuizzListCategory" Value="FBC83A15-59B6-4F52-9863-893A3BBA63AD"/>
  14:      <Property Key="QuizzFields" Value="BA0AD5C7-C8A4-413C-8DE2-578634474470"/>
  15:      <Property Key="QuizzContentType" Value="08BF6137-EDD2-4401-A536-2A37766D49AC"/>
  16:      <Property Key="QuizzListAnimals" Value="36541092-DC4F-42AC-820D-0B82E6A45E6B"/>
  17:    </Properties>
  18:  </Feature>

Je stocque dans les propriétés de la feature, la liste de toute les autres features que je veux activées puis dans le featureReceiver...

Quizz > QuizzFeatureReceiver

   1:          public override void FeatureActivated(SPFeatureReceiverProperties properties)
   2:          {
   3:              SPSite site = properties.Feature.Parent as SPSite;
   4:   
   5:              try
   6:              {
   7:                  foreach (SPFeatureProperty featureProp in properties.Feature.Properties)
   8:                  {
   9:                      Guid featureGuid = new Guid(featureProp.Value);
  10:                      site.Features.Add(featureGuid);
  11:                  }
  12:              }
  13:              catch (Exception ex)
  14:              {
  15:                  ULS.WriteError("Error activating Quizz Features. Message : " + ex.Message, LOGCATEGORY);
  16:                  ULS.WriteError("Rolling back...", LOGCATEGORY);
  17:   
  18:                  site.Features.Remove(properties.Feature.DefinitionId);
  19:              }
  20:          }

Le comportement est "on ne peut plus simple", j'itère donc sur chacune des propriétés et m'assure d'ajouter la feautre à la collection de features de la collection de site. On pourrait s'assurer du bon format de featureProp.value afin de ne pas se retrouver avec un Guid == null.

Mais le plus important est de bien s'occuper de la gestion d'erreur. Si jamais l'activation d'une feature génère une exception, je log l'erreur (ULS.WriteError est une méthode fournie par SharePointOfView sur Codeplex (www.codeplex.com/sharepointofview))  et lance la désactivation de la feature Quizz afin de me retrouver dans un environnement stable.

   1:          public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
   2:          {
   3:              SPSite site = properties.Feature.Parent as SPSite;
   4:   
   5:              try
   6:              {
   7:                  for (int i = properties.Feature.Properties.Count - 1; i >= 0; i--)
   8:                  {
   9:                      SPFeatureProperty featureProp = properties.Feature.PropertiesIdea as SPFeatureProperty;
  10:   
  11:                      Guid featureGuid = new Guid(featureProp.Value);
  12:   
  13:                      SPFeature feature = site.Features[featureGuid];
  14:   
  15:                      if (feature != null)
  16:                      {
  17:                          site.Features.Remove(featureGuid);
  18:                      }
  19:                  }
  20:              }
  21:              catch (Exception ex)
  22:              {
  23:                  ULS.WriteError("Error de-activating Quizz Features. Message : " + ex.Message, LOGCATEGORY);
  24:                  throw ex;
  25:              }
  26:          }

J'itère donc à l'envers sur chacune des propriétés et les désactive au fur et à mesure si elles ont bien réussi à s'activer précédemment.

Bien entendu, pour que tout celà fonctionne, il faut que chaque feature se charge de faire le "nettoyage" de sa partie, par exemple, pour la feature QuizzListCategory :

Quizz > QuizzListCategoryFeatureReceiver

   1:      public override void FeatureActivated(SPFeatureReceiverProperties properties)
   2:          {
   3:              SPSite site = properties.Feature.Parent as SPSite;
   4:   
   5:              SPWeb web = site.RootWeb;
   6:   
   7:              try
   8:              {
   9:                  string quizzListCategoryTitle = Localization.GetResource("Quizz_List_QuizzCategory_Title", RESFILE, web.Language);
  10:                  string quizzListCategoryDescription = Localization.GetResource("Quizz_List_QuizzCategory_Description", RESFILE, web.Language);
  11:   
  12:                  web.Lists.Add(quizzListCategoryTitle,
  13:                      quizzListCategoryDescription,
  14:                      "Lists/QuizzCategory",
  15:                      "00BFEA71-DE22-43B2-A848-C05709900100",
  16:                      100,
  17:                      "100",
  18:                      SPListTemplate.QuickLaunchOptions.On);
  19:              }
  20:              catch (Exception ex)
  21:              {
  22:                  ULS.WriteError(ex.Message, LOGCATEGORY);
  23:                  site.Features.Remove(properties.Feature.DefinitionId);
  24:                  throw ex;
  25:              }
  26:   
  27:              web.Dispose();
  28:          }
  29:   
  34:          public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
  35:          {
  36:              SPSite site = properties.Feature.Parent as SPSite;
  37:              SPWeb web = site.RootWeb;
  38:   
  39:              try
  40:              {
  41:                  string quizzListCategoryTitle = Localization.GetResource("Quizz_List_QuizzCategory_Title", RESFILE, web.Language);
  42:                  SPList quizzListCategory = web.Lists[quizzListCategoryTitle];
  43:                  quizzListCategory.Delete();
  44:              }
  45:              catch (Exception ex)
  46:              {
  47:                  ULS.WriteError(ex.Message, LOGCATEGORY);
  48:              }
  49:   
  50:              web.Dispose();
  51:          }

Au niveau de l'activation, j'essaye d'ajouter une liste "custom" mais si jamais il y a un problème, je logue l'erreur, j'essaye de désactiver la feature qui va essayer de supprimer la liste créer, puis je préviens la feature "Globale" en lui passant de nouveau l'exception. La désactivation essaye tout simplement de supprimer la liste.

Je vais améliorer un peu la gestion de cette feature d'activation et je la publierai sur notre projet commun SharePointOfView sous peu.

Je reviendrai dans les semaines à venir sur cette exemple de solution basée sur un Quizz (que j'expliquerais en détails à l'occasion) et en profiterai pour détailler deux trois fonctionnalité que je vais implementer.

<Philippe/>

SharePoint v4 : Developpement avec Visual Studio 2010

Voilà un sujet croustillant qui est depuis trop longtemps sous NDA et dont je voulais absolument vous parler.

Bonne nouvelle, la NDA est tombé après l'annonce de Paul Andrew lors du Teched EMEA à Barcelone, il y a quelque jours.(http://blogs.msdn.com/pandrew/archive/2008/11/10/visual-studio-2010-tools-for-sharepoint-announced-at-teched-emea-developers-2008.aspx)

Je peux donc prendre le temps de vous parler de ce dont on nous (MVP SharePoint) avait parler il y a quelques mois et (cerise sur le gateau) vous pourrez même visionner la vidéo sur Channel 9 :http://channel9.msdn.com/posts/VisualStudio/Sharepoint-Development-with-Visual-Studio-2010/ 

Cette vidéo nous montre Reza Chitsaz, Senior Program Manager qui travaille sur le développement Office et les Outils Sharepoint présentant les "Cool Features" de VS10 pour SharePoint

image

Reza commence donc par remettre dans le contexte en disant que le développement SharePoint est plutôt difficile, qu'ils ont eu beaucoup de feedbacks comme quoi il y a beaucoup d'efforts manuels à faire pour déployer des applications custom pour SharePoint.

Dorénavant, ils vont faire en sorte de simplifier tout le processus de développement dans SharePoint comme par exemple améliorer l'expérience F5 (Compile, Génération Solution, Add Solution, Attach le Debugger, Lancement de Internet Explorer, etc...).

Par contre, ca reste une V1 qui va se concentrer sur les RAD (Rapid Application Development) et le Designer et les scénarii principaux mais ils couvriront aussi le reste, plus globalement...

Voilà une liste des templates supportés à l'heure actuelle : (C# et VB supporté)

image

Le but est aussi d'avoir un meilleur contrôle de la génération de la solution WSP avec le Package Explorer (l'équivalent du WSP Explorer des VseWSS) et le Feature Explorer (qui permet de cliquer sur une feature du Package Explorer pour avoir plus d'infos sur les fichiers, etc...). On a aussi une nouvelle section dédié à SharePoint dans l'onglet Server Explorer qui apportent certaines fonctionnalités de SharePoint Manager 2007 ( http://www.codeplex.com/spm ). Cette outil permet d'afficher une arborescence décrivant le contenu de votre site SharePoint.

imageimageimage

Pour finir, on notera le support de MSBuild ce qui nous permettra de nous intégrer plus simplement à tout les processus comme les builds en continu avec Team Foundation Server.

Allez, franchement la vidéo dure 5 mn, ca vaut le coup de la regarder : http://channel9.msdn.com/posts/VisualStudio/Sharepoint-Development-with-Visual-Studio-2010/

</Philippe>

SharePoint 2007 : Patterns & Practices Guidance

Je vous en avais déjà parlé dans un précédent post, l'équipe P&P travaille depuis plusieurs mois sur un guide du développement SharePoint 2007.

Bonne nouvelle, après plusieurs mois de travail, ce guide est finalement terminé (cf l'annonce de Blaine Wastell)

Vous trouverez tout les documents et livrables sur la MSDN et sur Codeplex

 image

L'équipe P&P a travaillée main dans la main avec un groupe de MVP dont j'ai eu la chance de faire partie pour parfaire les bonnes pratiques de développements.

Au final vous trouverez énormément d'informations et d'exemples dont notamment un application complète nommée Training Management application (ci-dessous, l'architecture de l'application).

image

On y retrouvera aussi des concepts très intéressants comme :

  • L'utilisation de design patterns comme le Model-View-Presenter (MVP), le Repository, Service Locator.
  • L'utilisation de test unitaires avec TypeMock
  • Des guides permettant de prendre des décisions en connaissance de cause lors de choix techniques :
    • Utilisation de Définitions de sites
    • Type de contenu
    • List SharePoint vs Base de données
    • Définitions de listes
    • Customization de l'interface de liste SharePoint
    • Validation des données de listes
    • Utilisation de Webpart standard et custom
    • Workflow et Event Receivers
    • Implémentation de la sécurité

Et bien d'autres choses :

  • How to: Create a Custom Content Type with Event Receivers.
  • How to: Wrap a User Control Inside of a Web Part for SharePoint.
  • How to: Debug SharePoint Applications.
  • How to: Implement a SharePoint Workflow with ASP.NET Forms.
  • How to: Perform ASP.NET-Related Development with Visual Studio extensions for Windows SharePoint.
  • How to: Create an Automated Build and Deployment Solution with Team Foundation Server Team Build.
  • How to: Use Visual Studio extensions for Windows SharePoint Services to Package a Workflow in a Web Solution Package.
  • To understand where to locate application pages and content pages, see Location of Custom ASPX Pages in SharePoint.
  • To understand how to programmatically associate workflows with lists, see Associating Workflows with Lists.
  • To understand how to use custom field types and custom field controls, see Using Custom Field Types and Field Controls.
  • To understand how to organize SharePoint features, see Organizing Features.
  • To understand how to manage the memory used by SharePoint objects, see Memory Management for SharePoint Objects.
  • To understand how to staple a SharePoint feature to a site definition, see Stapling Features to Site Definitions.
  • To understand how to brand a site, see Branding with SharePoint Themes.
  • To learn how to use continuous integration in your SharePoint development environment, see Using Continuous Integration.
  • To learn how to create a SharePoint development environment that includes continuous integration, unit testing and build verification tests, see Team Development Overview.

    Autant dire que ça fait un moment que j'attendais la sortie officielle de ce guide (même si il est accessible depuis un certain temps sur codeplex). Je suis vraiment fan de ce que l'équipe P&P a créer et je suis persuadé que cela contribuera à améliorer la qualité des projets SharePoint par la suite.

    Personnellement, je suis très fan du projet SPGSharePointDataAccessQuickStart qui nous montre les différents moyens qu'ils ont envisagé pour accèder à SharePoint:

    • Accès direct
      image

    • List Item repository
      image

    • Announcement Repository
      image

    Bref, vous savez ce qu'il vous reste à faire :) (MSDN , Codeplex)

    <Phil/>


  • Plus de Messages Page suivante »

    Les 10 derniers blogs postés

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

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

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

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

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

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

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

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

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

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