Ce billet fait partie de la série de posts liés au développement sur SharePoint Online. Il fait suite au post précédent qui à pour but de montrer qu’on peut configurer automatiquement un site SharePoint hebergé dans le cloud de Microsoft en un seul clic.
Le but étant de partir du fameux site SharePoint classique pour arriver à un site complètement personnalisé en un clic de feature.

| Feature Désactivée | Feature Activée |
 |  |
Dans le précédent post, on a vu comment uploader les fichiers nécessaires à ce changement de look & feel et à les publier proprement (archiver, approuver et publier pour être précis) dans une solution Sandboxed exécutée sur un SharePoint Online.
Le but est maintenant de voir comment :
- Lier les MasterPages aux sites et assigner le nouveau layout à la page par défaut
- Insérer du contenu HTML dans la publishing page.
- Changer les valeurs de configuration de la recherche
Lier les Masterpages et autres dépendances graphiques
// Enumerate through each site and apply branding.
foreach (SPWeb web in siteCollection.AllWebs)
{ try
{ //Only if Search or Publishing webtemplate
if (web.WebTemplateId == 90 || web.WebTemplateId == 53)
{ if (web.WebTemplateId == 90)
web.CustomMasterUrl = webAppRelativePath + "_catalogs/masterpage/Synesthesia-search.master";
else
{ web.CustomMasterUrl = webAppRelativePath + "_catalogs/masterpage/Synesthesia-home.master";
web.MasterUrl = webAppRelativePath + "_catalogs/masterpage/Synesthesia.master";
web.CustomJavaScriptFileUrl = webAppRelativePath + "Style Library/Synesthesia/jquery.nivo.slider.js";
}
web.UIVersion = 4;
web.Update();
}
}
// Best Practices: Using Disposable Windows SharePoint Services Objects
// http://msdn.microsoft.com/en-us/library/aa973248.aspx
finally
{ if (web != null)
web.Dispose();
}
}
Dans ce bout de code (dans le lequel, je ne catche aucune exception…), j’itère sur les sites de ma collection et effectue le changement de mes masterpages uniquement sur les Search ou Publishing Webtemplate.
Il faut simplement savoir que j’utilise une masterpage sensiblement différente pour le site de recherche et que l’utilisation de CustomMasterUrl et MasterUrl dépend de la règle suivante :
- Les pages de publications utilisent le token ~masterurl/custom.master
- Toutes les autres pages standards (formulaires et view pages) utilisent le token ~masterurl.default.master
Ainsi, en utilisant CustomMasterUrl & MasterUrl, je change les valeurs de ces token dynamiquement et du coup les pages chargent les nouvelles masterpages.
Bien sur, on n’oublie pas de respecter les bonnes pratiques de développement et on pense à libérer explicitement le SPWeb
Nota Bene :
Si ce que je viens de vous dire est du chinois pour vous et que la page Best Practices: Using Disposable Windows SharePoint Services Objects (http://msdn.microsoft.com/en-us/library/aa973248.aspx) vous semble difficile à digérer, je vous conseille d’utiliser SPDisposeCheck http://archive.msdn.microsoft.com/SPDisposeCheck. SPDisposeCheck est un outil qui aide les développeurs et les administrateurs à vérifier que les développements SharePoint respecttent les bonnes pratiques en terme de libération des ressources.
C’est pratique, rapide et ca peut éviter que votre administrateur vous séquestre pendant 3 semaines parce que vos fuites mémoire ont fait crasher sa ferme SharePoint…
Assigner un nouveau modèle de page à la page d’acceuil
1: else
2: { 3: web.CustomMasterUrl = webAppRelativePath + "_catalogs/masterpage/Synesthesia-home.master";
4: web.MasterUrl = webAppRelativePath + "_catalogs/masterpage/Synesthesia.master";
5: web.CustomJavaScriptFileUrl = webAppRelativePath + "Style Library/Synesthesia/jquery.nivo.slider.js";
6:
7: // Unfortunately there is no EASY way to change a Pagelayout's Publishing page from a Sandbox Solution,
8: // So it's quick & dirty mode ON. Thx God for Reflector.
9: SPFile defaultPage = web.GetFile(web.ServerRelativeUrl + "pages/default.aspx");
10: string layoutUrl = webAppRelativePath + "_catalogs/masterpage/synesthesia.aspx";
11: string layoutDescription = "Synesthesia Page Layout";
12: SetPageLayout(defaultPage, layoutUrl, layoutDescription);
13:
14: string htmlContent = @"<br/><br/><br/><br/><div class='col'><h3>Quisque Vel Lorem</h3><div class='ft'><p>Quisque vel lorem eu libero laoreet facilisis. Aenean placerat, ligula quis placerat iaculis, mi magna luctus nibh, adipiscing pretium erat neque vitae augue.</p><p><a href='#'>Read More</a></p></div></div><div class='col'><h3>Placerat Iaculis</h3><div class='ft'><p>Quisque vel lorem eu libero laoreet facilisis. Aenean placerat, ligula quis placerat iaculis, mi magna luctus nibh, adipiscing pretium erat neque vitae augue.</p><p><a href='#'>Read More</a></p></div></div><div class='col last'><h3>Vitae Augue</h3><div class='ft'><p>Quisque vel lorem eu libero laoreet facilisis. Aenean placerat, ligula quis placerat iaculis, mi magna luctus nibh, adipiscing pretium erat neque vitae augue.</p><p><a href='#'>Read More</a></p></div></div>";
15: ReplacePublishingPageContent(defaultPage, htmlContent);
16: }
Ici, on a un petit problème… Dans un monde normal (comprenez dans une Solution de type Ferme), j’aurais accès au modèle objet complet de SharePoint et je pourrais faire ce genre de chose :
- Récupèrer mon PublishingWeb
- PublishingWeb pubWeb = PublishingWeb.GetPublishingWeb(web))
- Récupèrer mon PageLayout
- soit via pubWeb.GetAvailablePageLayouts() soit via une requête CAML
- Récupèrer ma page
- PublishingPage.GetPublishingPage(listItem)
- Assigner mon Layout
- Mettre à jour.
Sauf que dans un univers Sandboxed & (et en plus dans SharePoint Online), je n’ai pas accès au namespace Publishing. Donc il va falloir ruser :
Il faut savoir que dans SharePoint, tout objet hérite potentiellement de SPListItem et le reste n’est que Métadonnées supplémentaires. L’utilisation du namespace Publishing ne fait rien de plus que de rajouter une couche d’abstraction pour nous simplifier la vie au jour le jour.
Mais rien ne nous empêche de le faire à l’ancienne en prenant bien soin de lancer reflector avant et de voir ce qu’il y a dans la DLL en question :
Au final, vous l’aurez compris, la partie intéressante est dans la fonction SetPageLayout(defaultPage, layoutUrl, layoutDescription);
1: /// <summary>
2: /// Sets the page layout.
3: /// </summary>
4: /// <param name="page">The page.</param>
5: /// <param name="layoutUrl">The layout URL.</param>
6: /// <param name="layoutDescription">The layout description.</param>
7: private void SetPageLayout(SPFile page, string layoutUrl, string layoutDescription)
8: { 9: EnableChanges(page);
10:
11: SPListItem pageItem = page.Item;
12:
13: Guid pageLayoutFieldGuid = new Guid("0f800910-b30d-4c8f-b011-8189b2297094"); 14:
15: pageItem[pageLayoutFieldGuid] = new SPFieldUrlValue()
16: { 17: Url = layoutUrl,
18: Description = layoutDescription
19: };
20: pageItem.Properties["PublishingPageLayoutName"] = layoutDescription;
21: pageItem.Update();
22: page.Update();
23:
24: FinalizeChanges(page);
25: }
26:
27: /// <summary>
28: /// Finalizes the changes.
29: /// </summary>
30: /// <param name="file">The file.</param>
31: private void FinalizeChanges(SPFile file)
32: { 33: // Depending on the settings of the parent document library we may need to check in and/or (publish or approve) the file
34: if (file.Level == SPFileLevel.Checkout)
35: file.CheckIn(string.Empty, SPCheckinType.MajorCheckIn);
36: if (file.Level == SPFileLevel.Draft)
37: { 38: if (file.DocumentLibrary.EnableModeration)
39: file.Approve(string.Empty);
40: else
41: file.Publish(string.Empty);
42: }
43: }
44:
45: /// <summary>
46: /// Enables the changes.
47: /// </summary>
48: /// <param name="file">The file.</param>
49: private void EnableChanges(SPFile file)
50: { 51: if (file.RequiresCheckout && file.CheckOutType == SPFile.SPCheckOutType.None)
52: { 53: file.CheckOut();
54: }
55: }
Le bout de code qui nous intéresse ici est de la ligne 5 à 16 :
1: private void SetPageLayout(SPFile page, string layoutUrl, string layoutDescription)
2: { 3: …;
4:
5: SPListItem pageItem = page.Item;
6:
7: Guid pageLayoutFieldGuid = new Guid("0f800910-b30d-4c8f-b011-8189b2297094"); 8:
9: pageItem[pageLayoutFieldGuid] = new SPFieldUrlValue()
10: { 11: Url = layoutUrl,
12: Description = layoutDescription
13: };
14: pageItem.Properties["PublishingPageLayoutName"] = layoutDescription;
15: pageItem.Update();
16: page.Update();
17:
18: …;
19: }
Cette portion de code est plus ou moins directement importée du code que j’ai pu lire dans Reflector en regardant ce qu’il se passe quand on fait Page.Layout = newLayout
Au final, je récupère l’élément correspondant à la page, stocke l’url du nouveau modèle de page dans la bonne métadonnée, je rajoute la description dans le properties bag de l’élément et j’update le tout
Au final, le code est simplissime mais la façon de faire est un peu différente de ce qu’on est habitué à faire en général.
Insérer du contenu HTML dans la page d’accueil
…Qui est une page de publication donc à priori on va avoir le même problème… et donc la même solution !
/// <summary>
/// Replaces the content of the publishing page.
/// </summary>
/// <param name="page">The page.</param>
/// <param name="htmlContent">Content of the HTML.</param>
private void ReplacePublishingPageContent(SPFile page, object htmlContent)
{ EnableChanges(page);
SPListItem pageItem = page.Item;
// Can't Activate on SPO cause of FieldId, Publishing namespace.
//pageItem[FieldId.PublishingPageContent] = htmlContent;
Guid publishingPageContentFieldGuid = new Guid("F55C4D88-1F2E-4ad9-AAA8-819AF4EE7EE8"); pageItem[publishingPageContentFieldGuid] = htmlContent;
pageItem.Update();
page.Update();
FinalizeChanges(page);
}
On remarquera ici, une petite différence.
En effet, dans une sandbox développée sur mon environnement de développement le code pageItem[FieldId.PublishingPageContent] = htmlContent fonctionnait parfaitement.
Lors de mon passage sur l’environnement SharePoint Online, il m’a été impossible de déployer ma sandboxed solution parce que je faisais référence à FieldID. J’ai du récupèrer la valeur du champ PublishingPageContent et le spécifier en dur dans mon code.
Rien de bien grave mais c’est à noter qu’il peut y avoir des différences entre votre environnement Sandboxed et celui de SPO.
Changer les valeurs de configuration de la recherche
Et pour finir sur une petite pointe d’humour, je vais vous montrer comment arriver à changer les configurations liées à la recherche dans la collection de site :

La première chose qui surprend, c’est que vous n’avez aucune propriété à ce sujet sur votre SPSite… Du coup, mon premier réflexe a été de regarder le nom de la page applicative qui me permettait de faire ce changement dans SharePoint.
Ce qui m’a forcément ramener à Reflector et à examiner exactement ce qui se passait dans ce bout de code. Et bien figurez vous que les informations sont stockées à cet endroit :
- rootWeb.AllProperties["SRCH_ENH_FTR_URL"]
- rootWeb.AllProperties["SRCH_TRAGET_RESULTS_PAGE"]
Non, je n’ai pas fait de typos dans la chaine de caractères des propriétés... Du coup, j’applique le même code dans ma sandbox :
// Doesn't work (ie. DO NOTHING) when run inside a Sandbox Solution
// Cf : http://www.moss2007.be/blogs/vandest/archive/2011/03/06/sharepoint-spweb-properties-versus-spweb-allproperties.aspx
rootWeb.AllProperties["SRCH_ENH_FTR_URL"] = webAppRelativePath + "Search";
rootWeb.AllProperties["SRCH_TRAGET_RESULTS_PAGE"] = webAppRelativePath + "Search/Results.aspx";
rootWeb.Update();
Sauf que rien ne se passe… Aucune exceptions, messages d’erreur, rien dans les fichiers de log, rien... En mode debug, mon code est bien exécuté mais il ne change pas la valeur de la propriété.
Deuxième réflexe, un petit bing/google pour voir si je suis le seul à avoir fait ce genre de chose (la réponse est bien évidemment non), et je tombe sur un post de blog très intéressant http://www.moss2007.be/blogs/vandest/archive/2011/03/06/sharepoint-spweb-properties-versus-spweb-allproperties.aspx
En résumé :
- Si la propriété SRCH_ENH_FTR_URL n’est pas entièrement en majuscules, ca ne marchera pas.
- Si vous êtes dans un environnement Sandbox, SPWeb.AllProperties est uniquement accessible en ReadOnly. Mais si vous essayez de mettre à jour le table de Hash, aucune exception ne sera lancée, il ne se passera rien tout simplement.
Néanmoins, je reste persuadé qu’il est possible de changer cette valeur peut être pas par code mais de manière littérale (XML), peut être à l’instanciation du site… A voir.
Dans tout les cas, voilà qui clôt le sujet sur la personnalisation graphique, je pensais parler dans mes prochains billets des possibilités de SharePoint Online & Silverlight ou potentiellement d’une application complète de Covoiturage avec BingMaps.
Si vous avez d’autres idées, je serai ravi de les entendre.
<Phil/>