SharePoint 2010 : Développement SharePoint Online, Personnalisation Graphique et Packaging – Partie 1

Ce billet fait partie de la série de posts liés au développement sur SharePoint Online.

Le but de cette série est de montrer/démontrer/prouver qu’il est tout à fait possible de développer des composants voire des applications complètes directement dans le Cloud de Microsoft (dans le contexte SharePoint).

Le cœur de ce post se concentrera sur un des premiers besoins de la majorité des entreprises qui font le choix Sharepoint Online : La personnalisation graphique.

Si vous avez suivi ma session avec Stéphanie durant les Techdays 2011 concernant les Best Practices de développement pour Sharepoint Online, vous savez déjà qu’il est possible de faire ce genre de chose via SharePoint Designer (@6:40 dans la vidéo).

Naturellement, il est impossible de répondre à une demande de personnalisation graphique avec un outil comme SPD de manière professionnelle. Je ne connais aucun IT SharePoint qui vous laisserait faire ce genre de modification directement sur le serveur en live (par contre, j’en connais au moins un qui me taperait littéralement dessus si je m’y risquais).

Sans compter que vous ne pouvez pas réellement automatiser cette personnalisation; en effet, si vous avez plusieurs sites à configurer, il vous faudra répéter vos actions de configuration/personnalisation pour chacun des sites * environnement de qualification = risques d’ennui et de fautes de manipulations garanties.

Bon maintenant que SPD est hors du scope de ce post, il ne nous reste plus qu’à se demander comment faire avec VisualStudio 2010 :

Dans le cadre de ce post, je pars d’un design fourni sur le site TopSharePoint

http://www.topsharepoint.com/synesthesia-%E2%80%93-free-sharepoint-2010-theme

Synesthesia – Free SharePoint 2010 Theme

Mon objectif est de vous montrer qu’il est possible d’automatiser toutes les tâches qui sont détaillées dans le post ci-dessus :

  • Upload des fichiers Masterpages, images, css & scripts

Synesthesia – Free SharePoint 2010 Theme image15     Synesthesia – Free SharePoint 2010 Theme

  • Lier les MasterPages aux sites

 

  • Changer le modèle de page de la page d’acceuil

  • Rajouter du contenu HTML

  • Changer les configurations liées à la recherche dans la collection de site

Première étape : Création et Agencement du projet

image14

Rien de bien compliqué ! Une feature, 2 Elements Manifest qui vont respectivement uploadés les pagesLayouts, les Masterpages et les Images, css, javascript.

  • Synesthesia.Designer

image17

  • Masterpages >> Elements.xml
<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
    <Module Name="Masterpages" Url="_catalogs/masterpage">
        <File Path="Masterpages\synesthesia.master" Url="synesthesia.master" >
            <Property Name="UIVersion" Value="4" />
            <Property Name="ContentTypeId" Value="0x010105" />
        </File>
        <File Path="Masterpages\synesthesia-home.master" Url="synesthesia-home.master" >
            <Property Name="UIVersion" Value="4" />
            <Property Name="ContentTypeId" Value="0x010105" />
        </File>
        <File Path="Masterpages\synesthesia-search.master" Url="synesthesia-search.master" >
            <Property Name="UIVersion" Value="4" />
            <Property Name="ContentTypeId" Value="0x010105" />
        </File>
        <File Path="Masterpages\synesthesia.aspx" Url="synesthesia.aspx" Type="GhostableInLibrary">
            <Property Name="Title" Value="Synesthesia Page Layout" />
            <Property Name="ContentType" Value="$Resources:cmscore,contenttype_pagelayout_name;" />
            <Property Name="PublishingPreviewImage" Value="~SiteCollection/_catalogs/masterpage/$Resources:core,Culture;/Preview Images/ArticleBodyOnly.png, ~SiteCollection/_catalogs/masterpage/$Resources:core,Culture;/Preview Images/ArticleBodyOnly.png" />
            <Property Name="PublishingAssociatedContentType" Value=";#$Resources:cmscore,contenttype_articlepage_name;;#0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF3900242457EFB8B24247815D688C526CD44D;#"/>
        </File>
    </Module>
    <Module Name="DoStuffOnUploadedFiles">
        <File Path="Masterpages\Elements.xml" Url="Elements_Masterpages_$SharePoint.Feature.Id$.xml" Type="Ghostable" />
    </Module>
</Elements>
  • StyleLibrary >> Element.xml
<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <Module Name="StyleLibrary" RootWebOnly="TRUE" Url="Style Library">
    <File Path="StyleLibrary\Synesthesia\synesthesia.css" Url="Synesthesia/synesthesia.css" />
    <File Path="StyleLibrary\Synesthesia\images\bullets.png" Url="Synesthesia/images/bullets.png" />
    <File Path="StyleLibrary\Synesthesia\images\delicious.png" Url="Synesthesia/images/delicious.png" />
    <File Path="StyleLibrary\Synesthesia\images\facebook.png" Url="Synesthesia/images/facebook.png" />
    <File Path="StyleLibrary\Synesthesia\images\favicon.ico" Url="Synesthesia/images/favicon.ico" />
    <File Path="StyleLibrary\Synesthesia\images\feed.png" Url="Synesthesia/images/feed.png" />
    <File Path="StyleLibrary\Synesthesia\images\friendfeed.png" Url="Synesthesia/images/friendfeed.png" />
    <File Path="StyleLibrary\Synesthesia\images\home1.jpg" Url="Synesthesia/images/home1.jpg" />
    <File Path="StyleLibrary\Synesthesia\images\home1a.jpg" Url="Synesthesia/images/home1a.jpg" />
    <File Path="StyleLibrary\Synesthesia\images\home2.jpg" Url="Synesthesia/images/home2.jpg" />
    <File Path="StyleLibrary\Synesthesia\images\home2a.jpg" Url="Synesthesia/images/home2a.jpg" />
    <File Path="StyleLibrary\Synesthesia\images\home3.jpg" Url="Synesthesia/images/home3.jpg" />
    <File Path="StyleLibrary\Synesthesia\images\home4.jpg" Url="Synesthesia/images/home4.jpg" />
    <File Path="StyleLibrary\Synesthesia\images\home4a.jpg" Url="Synesthesia/images/home4a.jpg" />
    <File Path="StyleLibrary\Synesthesia\images\leftBG.png" Url="Synesthesia/images/leftBG.png" />
    <File Path="StyleLibrary\Synesthesia\images\linkedin.png" Url="Synesthesia/images/linkedin.png" />
    <File Path="StyleLibrary\Synesthesia\images\loading.gif" Url="Synesthesia/images/loading.gif" />
    <File Path="StyleLibrary\Synesthesia\images\mainBg.gif" Url="Synesthesia/images/mainBg.gif" />
    <File Path="StyleLibrary\Synesthesia\images\mainBG.jpg" Url="Synesthesia/images/mainBG.jpg" />
    <File Path="StyleLibrary\Synesthesia\images\mainBG1.png" Url="Synesthesia/images/mainBG1.png" />
    <File Path="StyleLibrary\Synesthesia\images\mainContentBG.png" Url="Synesthesia/images/mainContentBG.png" />
    <File Path="StyleLibrary\Synesthesia\images\navBarFill.png" Url="Synesthesia/images/navBarFill.png" />
    <File Path="StyleLibrary\Synesthesia\images\navBarLeft.png" Url="Synesthesia/images/navBarLeft.png" />
    <File Path="StyleLibrary\Synesthesia\images\navBarRight.png" Url="Synesthesia/images/navBarRight.png" />
    <File Path="StyleLibrary\Synesthesia\images\navBarSep.png" Url="Synesthesia/images/navBarSep.png" />
    <File Path="StyleLibrary\Synesthesia\images\navBlue.gif" Url="Synesthesia/images/navBlue.gif" />
    <File Path="StyleLibrary\Synesthesia\images\next.png" Url="Synesthesia/images/next.png" />
    <File Path="StyleLibrary\Synesthesia\images\prev.png" Url="Synesthesia/images/prev.png" />
    <File Path="StyleLibrary\Synesthesia\images\rss.png" Url="Synesthesia/images/rss.png" />
    <File Path="StyleLibrary\Synesthesia\images\shadow-bottom.png" Url="Synesthesia/images/shadow-bottom.png" />
    <File Path="StyleLibrary\Synesthesia\images\shadow-left.png" Url="Synesthesia/images/shadow-left.png" />
    <File Path="StyleLibrary\Synesthesia\images\shadow-right.png" Url="Synesthesia/images/shadow-right.png" />
    <File Path="StyleLibrary\Synesthesia\images\srcBG.png" Url="Synesthesia/images/srcBG.png" />
    <File Path="StyleLibrary\Synesthesia\images\ss_back.png" Url="Synesthesia/images/ss_back.png" />
    <File Path="StyleLibrary\Synesthesia\images\synesthesia.png" Url="Synesthesia/images/synesthesia.png" />
    <File Path="StyleLibrary\Synesthesia\images\twitter.png" Url="Synesthesia/images/twitter.png" />
    <File Path="StyleLibrary\Synesthesia\images\youtube.png" Url="Synesthesia/images/youtube.png" />
    <File Path="StyleLibrary\Synesthesia\images\arrows.png" Url="Synesthesia/images/arrows.png" />
    <File Path="StyleLibrary\Synesthesia\images\background.png" Url="Synesthesia/images/background.png" />
    <File Path="StyleLibrary\Synesthesia\images\nemo.jpg" Url="Synesthesia/images/nemo.jpg" />
    <File Path="StyleLibrary\Synesthesia\images\toystory.jpg" Url="Synesthesia/images/toystory.jpg" />
    <File Path="StyleLibrary\Synesthesia\images\up.jpg" Url="Synesthesia/images/up.jpg" />
    <File Path="StyleLibrary\Synesthesia\images\walle.jpg" Url="Synesthesia/images/walle.jpg" />
    <File Path="StyleLibrary\Synesthesia\jquery-1.4.3.min.js" Url="Synesthesia/jquery-1.4.3.min.js" />
    <File Path="StyleLibrary\Synesthesia\jquery.nivo.slider.js" Url="Synesthesia/jquery.nivo.slider.js" />
  </Module>
    <Module Name="DoStuffOnUploadedFiles">
        <File Path="StyleLibrary\Elements.xml" Url="Elements_StyleLibrary_$SharePoint.Feature.Id$.xml" Type="Ghostable" />
    </Module>
</Elements>

On déploie, on active, les fichiers sont bien uploadés mais bon… on a un petit souci. Tout les fichiers sont en Checkout, numero de version 0.1…

Si vous faites un petit coup de Bing/Google, vous trouverez rapidement une solution de contournement : http://stefan-stanev-sharepoint-blog.blogspot.com/2011/01/automatically-publishing-files.html

En résumé, dans une sandboxed solution, toute l’API n’est pas disponible. Notamment le fameux GetElementDefinitions qui permet de lire les Element.xml décrivant les features. En général, on utilise cette méthode pour traiter un grand nombre de fichier uploadés. C’est notamment comme ça que j’avais développé à l’époque l’extension de méthode SoVDeleteUploadedFiles sur le post SharePoint 2007 : Extension Method (DeleteUploadedFiles). Vous pourrez retrouver directement le bout sur code sur le projet Codeplex SharePointOfView à l’adresse suivante : http://sharepointofview.codeplex.com/SourceControl/changeset/view/18307#244105

Heureusement, il y a plusieurs solutions alternatives dont notamment une, que je trouve relativement élégante vu le contexte. Cette solution consiste à dire que si on ne peut pas avoir le fichier dans l’API restreinte des Sandboxed Solutions, on va l’obtenir en uploadant le fichier dans SharePoint et y accéder en .Net classique à coup de StreamReader & Xdocument.

Le code est basé sur le travail de Stefan Stanev que j’ai refactoré & étendu pour répondre à mes besoins particuliers pour cette solution :

 
/// <summary>
/// Does stuff on uploaded files.
/// Code base from :http://stefan-stanev-sharepoint-blog.blogspot.com/2011/01/automatically-publishing-files.html
/// Refactored to handle Action parameter.
/// </summary>
/// <param name="web">Current SPWeb</param>
/// <param name="featureID">Feature used to upload all the files</param>
/// <param name="deleteFiles">if set to <c>true</c> [delete files].</param>
/// <param name="toDoList">To do list.</param>
private void DoStuffOnUploadedFiles(SPWeb web, Guid featureID, bool deleteFiles, List<Action<SPFile>> toDoList)
{
    // Create a regular expression pattern for the manifest files
    string pattern = string.Format(@"^.+_{0}.xml$", featureID);
    Regex fileNameRE = new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);
    XNamespace WS = "http://schemas.microsoft.com/sharepoint/";
 
    // Get the manifest files from the root folder of the site
    SPFile[] manifestFiles = web.RootFolder.Files.Cast<SPFile>().Where(f => fileNameRE.IsMatch(f.Name)).ToArray();
    try
    {
        // Iterate the manifest files
        foreach (SPFile manifestFile in manifestFiles)
        {
            // Load the contents of the manifest file in an XDocument
            MemoryStream mStream = new MemoryStream(manifestFile.OpenBinary());
            StreamReader reader = new StreamReader(mStream, true);
            XDocument manifestDoc = XDocument.Load(reader, LoadOptions.None);
 
            // Iterate over the 'Module' and 'File' elements in the XDocument, concatenating their Url attributes in a smart way so that we grab the site relative file Url-s
            string[] fileUrls = manifestDoc.Root.Elements(WS + "Module")
                .SelectMany(me => me.Elements(WS + "File"), (me, fe) => string.Join("/", new XAttribute[] { me.Attribute("Url"), fe.Attribute("Url") }.Select(attr => attr != null ? attr.Value : null).Where(val => !string.IsNullOrEmpty(val)).ToArray()))
                .ToArray();
 
            // Iterate the file url-s
            foreach (string fileUrl in fileUrls)
            {
                // Get the file
                SPFile file = web.GetFile(fileUrl);
 
                foreach (Action<SPFile> func in toDoList)
                {
                    func(file);
                }
            }
        }
    }
    finally
    {
        if (deleteFiles)
        {
            // finally delete the manifest files from the site root folder
            foreach (SPFile manifestFile in manifestFiles)
                manifestFile.Delete();
        }
    }
}

Le code est relativement simple (et ne gère pratiquement aucun cas d’erreur).Une fois le fichier élément.xml récupéré depuis SharePoint, il est analysé et on récupère la liste des fichiers uploadés. Pour chacun de ces fichiers, on exécutera une liste de fonction données (ToDoList).

Le fichier Element.xml est uploadé ainsi (à la fin de Masterpage >> Element.xml) :
 

    <Module Name="DoStuffOnUploadedFiles">
        <File Path="Masterpages\Elements.xml" Url="Elements_Masterpages_$SharePoint.Feature.Id$.xml" Type="Ghostable" />
    </Module>

Notez l’utilisation intelligente des tokens : Elements_Masterpages_$SharePoint.Feature.Id$.xml pour le rendre unique.

La fonction est appelée ainsi dans la méthode FeatureActivated :

     var toDoList = new List<Action<SPFile>>() { new Action<SPFile>(FinalizeChanges)};
    DoStuffOnUploadedFiles(rootWeb, properties.Feature.DefinitionId, false, toDoList);

Voilà la fonction qui va fait tout le boulot demandé :

/// <summary>
/// Finalizes the changes.
/// </summary>
/// <param name="file">The file.</param>
private void FinalizeChanges(SPFile file)
{
    // Depending on the settings of the parent document library we may need to check in and/or (publish or approve) the file
    if (file.Level == SPFileLevel.Checkout)
        file.CheckIn(string.Empty, SPCheckinType.MajorCheckIn);
    if (file.Level == SPFileLevel.Draft)
    {
        if (file.DocumentLibrary.EnableModeration)
            file.Approve(string.Empty);
        else
            file.Publish(string.Empty);
    }
}

Et voilà, toute ma petite 50aine de fichier Uploadés, Checkin, Approuvés et Publiés !

Pour la touche finale, on peut rajouter cette ligne de code sur le FeatureDeactivating qui supprimera tout les fichiers Uploadés.

    // Clean up
    // Delete set to false car the delegate is going to delete all files in the Element.xml
    var toDoList = new List<Action<SPFile>>() { delegate(SPFile file) { file.Delete(); } };
    DoStuffOnUploadedFiles(topLevelsite, properties.Feature.DefinitionId, false, toDoList);

Maintenant que nous avons une méthode propre et surtout qui s’active/désactive correctement, on verra dans le prochain post :

  • Comment lier les MasterPages aux sites et assigner le nouveau layout à la page par défaut
  • Comment insérer du contenu HTML dans la publishing page.
  • Comment changer les valeurs de configuration de la recherche

<Phil/>

Publié lundi 23 mai 2011 10:00 par phil
Classé sous , , , , , , , ,
Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :

Commentaires

# re: SharePoint 2010 : Développement SharePoint Online, Personnalisation Graphique et Packaging – Partie 1 @ mercredi 15 juin 2011 21:39

Excellent article !

minsou

About phil

Philippe Sentenac est Consultant SharePoint à Wygwam en région Parisienne. Il intervient essentiellement sur des missions liées à SharePoint (2007 et 2010 ) mais aussi autour du Web 2.0. Plus généralement, il s'intéresse à l'ASP.Net (MVC) , à Silverlight, et à tout ce qui est orienté Web en rapport avec les nouvelles technologies, qu'il pratique depuis 2006. Féru de développement, il est passionné par les problématiques de méthodologies et d'industrialisation du développement.

Les 10 derniers blogs postés

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

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

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

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

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

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

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

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

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

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