Archives de catégorie : Sites collaboratif

Créer des filtres dynamiques sur vos List View WebPart avec du JS + KnockoutJS

Introduction

Jusqu’à tout récemment, j’utilisait une WebPart de filtre avec une connexion afin de filtrer l’année de publication des nouvelles :

Actualites

Cependant, ça ne fonctionne pas lorsqu’on utilise la pagination ET le filtre.  Le problème c’est que la pagination ajoute les paramètres de filtre directement dans le Querystring Ex : FilterField1=PublishedDateYear&FilterValue1=2016. Par la suite, même si on change de valeur via la WebPart de filtre, la valeur qui sera appliqué sera toujours celle du Querystring.

Je n’ai malheureusement pas trouvé de solution élégante à cette problématique alors j’ai plutôt opté pour une solution en JavaScript qui m’offre plus de flexibilité.

Une solution 100 % JS

Je me suis basé sur un article que j’ai trouvé sur le blog de Phil Harding et je l’ai adapté à mon besoin. Le résultat est très intéressant et nous allons voir en détail comment mettre le tout en place dans ce billet. Voici tout d’abord un bref aperçu :

ActualitesFiltreDynamique

Le code peut être exécuté autant dans SP2010 que SP2013 mais il y a quelque subtilité au niveau du code.

Intégration du code

La solution utilise une WebPart Éditeur de contenu pointant vers un fichier « txt » qui contient le balisage HTML. Ce fichier « txt » contient des références vers un fichier JS, CSS et au Framework Knockout JS v3.1.0.

La structure des fichiers est la suivante :

  • /Style Library/txt/script_actualites_filtre_html.txt (Gist)
  • /Style Library/css/app_filtre.css (Gist)
  • /Style Library/scripts/app_filtre.js (Gist)
  • /Style Library/scripts/knockout-3.1.0.js (Source)

La première étape consiste à copier vos fichiers dans votre dossier Style Library et à ajuster les références.

Ensuite, dans une page contenant une List View WebPart ajouter une WebPart Éditeur de contenu avec un lien vers le fichier : /Style Library/txt/script_actualites_filtre_html.txt.

Puis, adapter le fichier app_filtre.js (Voir les TODO dans le code) afin que vos filtres correspondent à vos colonnes et à votre liste.

app_filtre.js


(function(module,$j) {
"use strict";

window.pd = window.pd || {};

pd.MeetingsViewModel = function() {
   /* observable state */
   this.FilterTextIm = ko.observable('');
   this.FilterText = ko.computed(this.FilterTextIm)
                        .extend({ throttle: 400 });
   this.LibraryTitle = ko.observable('');
   this.LibrarySubTitle = ko.observable('');
   this.FilterYears = new ko.observableArray([]);
   this.FilterMonths = new ko.observableArray(['Janvier','Février','Mars','Avril','Mai','Juin','Juillet','Août','Septembre','Octobre','Novembre','Décembre']);
   this.SelectedYear = ko.observable('');
   this.SelectedMonth = ko.observable('');
   this.Working = ko.observable(true);

   /* populate years filter as Current Year..-=4 */
   for(var cy = parseInt(new Date().getFullYear()), y = cy; y > (cy-3); y--) { this.FilterYears.push(y.toString()); }

   /* model actions */
   this.filterByYear = function(year) {
      this.FilterTextIm('');   // zap the filter text
      year = (year === this.SelectedYear()) ? '' : year;
      this.SelectedYear(year);
   }.bind(this);
   this.filterByMonth = function(month) {
      this.FilterTextIm('');   // zap the filter text
      month = (month === this.SelectedMonth()) ? '' : month;
      this.SelectedMonth(month);
   }.bind(this);

   /* model behaviour */
   this.isSelectedYear = function (year) {
      var y = this.SelectedYear();
      return (y == year);
   }.bind(this);
   this.isSelectedMonth = function (month) {
      var m = this.SelectedMonth();
      return (m == month);
   }.bind(this);
}


pd.FilterLibraryApp = function() {
   var
      viewModel = new pd.MeetingsViewModel(),
      debug = true,
      wpCtx = null,
      wpEvent = null,
      
      filterControlId = null,
      listViewTableId = null,
      _module = {
         start: start
      };
   return _module;

   function locateListView() {
      var fn = function() {
         InitAllClvps();

         // find the CTX for the LVWP
         for(var k in g_ctxDict) {
           if (debug && window.console) console.log(g_ctxDict[k]);
           
           if (g_ctxDict[k].ListTitle === module.LibraryTitle) {
             wpCtx = window['ctx'+g_ctxDict[k].ctxId];
             break;
           }
         }
         if (debug && window.console) {
            console.log(wpCtx.clvp);
            console.log(wpCtx.clvp.ctx);
            console.log(wpCtx.clvp.ctx.view);
         }
         /* this isn't used in the 2010 version, not figured out if/how to use it yet 
         var clvp = (wpCtx && wpCtx.clvp) ? wpCtx.clvp : null;
         wpEvent = { clvp: clvp, event: { currentCtx: { clvp: clvp } } };
         */
         
         // find the LVWP and table containing the rows
         var 
            $wpnode = $j('#'+wpCtx.clvp.wpq).first(),
            $wpTableNode = $wpnode.find("table[id^='onetidDoclibViewTbl']");
         listViewTableId = $wpTableNode.attr('id');

		 //Il faut avoir préalablement coché la case "Afficher le bouton de raffraichisement" sur la LVWP

         // find the postback control id; e.g. ctl00$m$g_109629d4_d78b_4c9e_8ec0_90078b6e444e$ctl02
         var
            $wpinput = $wpnode.find('> :first-child > input:first-child'),
            ctrlid = $wpinput.attr('id');
         ctrlid = ctrlid && ctrlid.length
                     ?  ctrlid.replace(/^(ctl\d\d)_/gi,'$1$')
                               .replace(/_g_/g,'$g_')
                               .replace(/_(ctl\d\d)$/gi,'$$$1')
                     : '';
         if (debug && window.console) { console.log(">>locateListView: controlid="+ctrlid); }
         filterControlId = ctrlid;
         viewModel.LibraryTitle(wpCtx.ListTitle);
         viewModel.LibrarySubTitle('All');
         viewModel.Working(false);
      };
      EnsureScript("inplview", typeof InitAllClvps, fn);
   }

   function onFilterChange(filterType) {
      if (!filterControlId) {
         alert('Unable to filter: the list view webpart could not be located or the list view contains a Person/Group column!');
         return;
      }

      var
         filterColumn = null,
         filterValue = '';

      if (filterType.match(/year/gi)) {
         filterValue = this.SelectedYear();
         filterColumn = 'PublishedDateYear'; //TODO ajuster votre filtre
      } else if (filterType.match(/month/gi)) {
         filterValue = this.SelectedMonth();
         filterColumn = 'PublishedDateMonth'; //TODO ajuster votre filtre
      }

      if (!filterValue) filterValue = '##dvt_all##';
      var filterCall = "__doPostBack('"+filterControlId+"','NotUTF8;__filter={"+filterColumn+"="+filterValue+"}')"

      if (debug && window.console) {
         console.log(">>Filter by " + filterType + " on ["+filterColumn+"] = ["+filterValue+"]");
         console.log("  >> " + filterCall);
      }
      eval(filterCall);
   }

   function start() {
      ko.applyBindings(viewModel, $j('#cdtm').get(0));
      viewModel.SelectedYear.subscribe(onFilterChange.bind(viewModel, 'year'));
      viewModel.SelectedMonth.subscribe(onFilterChange.bind(viewModel, 'month'));

      locateListView();
   }
}();

$j(pd.FilterLibraryApp.start);

})({ Name: 'Module', LibraryTitle: 'Billets' },jQuery); //TODO Ajuster le titre de votre liste


Dernier détail

Afin de pouvoir lier les filtres à la vue, pour ma part j’ai été contraint de cocher la case suivante :

ActualitesFiltreDynamiqueLVWP

Sans cela, le controlid était toujours vide. Si vous avez une erreur vérifier tout d’abord votre console (F12) :

ActualitesFiltreDynamiqueLVWPConsole

Conclusion

Seulement avec du JS on a réussi à ajouter des filtres dynamiques sur une List View WebPart et cela fonctionne correctement avec la pagination. Donc, beaucoup de flexibilité au niveau du déploiement et de la personnalisation visuelle.

Référence

Dynamically Filtering SharePoint List Views using Javascript

Envoyer des courriels périodiques pour SharePoint OnPrem avec du PowerShell

Introduction

SharePoint dispose d’un mécanisme d’alerte assez puissant. Celui-ci permet d’envoyer à l’abonné une alerte par courriel basé sur une action qui se produit sur l’élément (il est créé, modifié, etc…). Il comporte cependant des limitations :

  • On ne peut pas recevoir une alerte périodique (ex : tous les trimestres, tous les premiers Lundi du mois, etc…);
  • La modification du gabarit de courriel est complexe;
  • Il n’est pas possible de modifier l’adresse de réponse et le sujet pour une alerte en particulier;
  • Vous devez être que propriétaire du site afin d’envoyer des alertes à d’autres personnes. Sans cela, la seule personne à qui vous pouvez envoyer des alertes à est vous-même.

Alors que faire si on veut contourner ces limitations?

Il y a quelques solutions possibles :

  1. Créer un timer job SharePoint (Exemple)
  2. Créer un flux de travail (Exemple)
  3. Utiliser un logiciel tiers (Exemple)
  4. Créer un script PowerShell s’exécutant dans une tâche planifié (Source)

Je vais mettre l’emphase sur le point 4 dans ce billet car à mon avis c’est la meilleure alternative lorsque les alertes « out-of-the-box » ne répondent pas à votre besoin.

Comparaison des différentes solutions

Alertes SharePoint out-of the-box Timer Job personnalisé

 

Flux de travail personnalisé Outil tiers

Script PowerShell

Flexibilité Mark Mark
Permet de modifier le contenu dynamique envoyé facilement Mark Mark Mark Mark
Permet de modifier le visuel du gabarit sans avoir besoin de programmation Mark Mark Mark
Envoyer des courriels à des utilisateurs externes Mark Mark
Nécessite peu d’efforts de configuration Mark Mark
Permet d’envoyer un courriel basé sur un intervalle de temps Mark Mark Mark
Gratuit Mark Mark Mark Mark

 

Utiliser un script PowerShell

Le script pour SharePoint 2013 sur site (OnPrem) est disponible ici :

telecharger-bouton

Assurez-vous de modifier la section « Constantes » et d’y inscrire les valeurs correspondants à votre environnement.

Capture

Celui-ci va créer une liste EmailTemplate :

EmailTemplate

Cette liste permet à un utilisateur de piloter les différentes valeurs sans avoir besoin d’effectuer de la programmation lorsque vient le temps de changer le contenu, le sujet, etc… Bien évidemment, si vous ajoutez d’autres modèles, vous devrez ajuster votre code afin de les utiliser correctement.

Le script va aussi créer une liste Project :

ProjectListView

Celle-ci est uniquement présente à des fins de démonstration. C’est à partir de cette liste que nous allons envoyer du contenu dynamique par courriel. Vous pouvez donc la supprimer et vous alimenter de votre propre liste si vous le souhaitez.

Voici un exemple de courriel envoyé par le script :

EmailResult

Exécution du script PowerShell dans une tâche planifiée

Démarrer -> Exécuter -> Taskschd.msc

Créer une nouvelle tâche :

ScheduledTaskPowerShell_create

Spécifier un compte ayant les droits d’administrateur de la ferme SharePoint.

Spécifier un déclencheur :

ScheduledTaskPowerShell_declencheur

Ajouter une nouvelle action :

ScheduledTaskPowerShell_action

Programme/Script : Powershell.exe

Ajouter des arguments : -ExecutionPolicy Bypass C:\Sources\SharePointerie.OnPrem.SendTimeBasedEmail.ps1

En prenant soin d’adapter le chemin vers votre script.

Cliquer sur OK pour enregistrer la tâche.

Outils de développeur pour faciliter le travail avec les courriels

Pour ma part, j’utilise l’outil SMTP4DEV afin de travailler en mode local. Il suffit de démarrer l’application et celle-ci se chargera d’intercepter les envois de courriel sur le port défini.

smtp4Dev

Source : https://smtp4dev.codeplex.com/

Conclusion

L’utilisation d’un script PowerShell pour envoyer des courriels périodiques permet un maximum de flexibilité autant au niveau du déploiement qu’au niveau du pilotage. Il n’y a pas de période d’indisponibilité lors du déploiement comparativement au déploiement d’un Timer Job. Je crois donc que c’est la meilleure alternative lorsque les alertes out-of-the-box ne répondent pas à votre besoin.

Techniques pour récupérer les éléments HTML d’une WebPart en JavaScript

Introduction

Dans ce billet je vous propose quelque techniques de JavaScript pour pouvoir récupérer les éléments HTML d’une WebPart spécifique. Chez un de mes clients je me suis vite rendu compte qu’il n’était pas évident d’avoir une « poigné » fixe pour appliquer une personnalisation sur une seule WebPart. Le Client Side Rendering (CSR) est à la mode c’est temps-ci mais il comporte une certaine complexité en comparaison aux quelques lignes de JavaScript qui suivront. Pour ce qui est du CSS, il n’est pas évident de l’appliquer seulement à une WebPart.

Examinons le code généré d’une WebPart

En appuyant sur F12 dans une page contenant des WebParts, vous pourrez examiner le code HTML généré par celle-ci.

Voici un exemple :

WebPartByTitle_HTMLElement

Vous remarquerez qu’il y a un ID unique sur chacune des WebParts de la page.

L’élément DIV avec le ID « MSOZoneCell_WebPartWPQ6 » correspond à l’ensemble de la WebPart.

L’élément DIV avec le ID « WebPartWPQ6_ChromeTitle » correspond à l’entête de la WebPart.

L’élément SPAN avec le ID « WebPartTitleWPQ6 » correspond au titre de la WebPart.

Voyons maintenant quelque techniques pour les récupérer.

Technique 1 – Récupérer les éléments HTML d’une WebPart par son titre

Cette technique consiste à utiliser le titre de la WebPart comme poigné pour récupérer les éléments HTML d’une WebPart précise.

Voici le code pour y arriver :

<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script type="text/javascript">
$(document).ready(function()
{
     //Parcours tous les éléments SPAN ayant un ID qui commencent par WebPartTitleWPQ
     $("span[id^=WebPartTitleWPQ]").each(function()
     {
          //Si le titre de la WebPart commence par "Site Collection Documents"
          if($(this).attr('title').indexOf("Site Collection Documents") == 0){
          //Applique une personnalisation sur l'ensemble de la WebPart (Couleur rouge)
               $(this).parent().parent().css('background-color', 'red');
          }
    });
});
</script>

Résultat

Le background-color rouge est appliqué uniquement sur la WebPart avec le titre « Site Collection Documents » :

WebPartByTitle_After

Points à faire attention avec cette technique

  1. Assurez-vous d’avoir un titre unique pour vos WebParts
  2. Assurez-vous d’utiliser un type de chrome incluant le « Titre »

Au besoin, vous pouvez modifier le titre de votre WebPart ou le type de chrome :

WebPartByTitle_MenuWP

Technique 2 – Récupérer les éléments HTML d’une WebPart par son ID

Cette technique consiste à utiliser l’ID comme poigné pour récupérer les éléments HTML d’une WebPart précise.

Voici un exemple de code pour modifier une WebPart par sont ID :

<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script type="text/javascript">
$(document).ready(function()
{
 //Applique une personnalisation à l'ensemble de la WebPart (Couleur bleu) par son ID
 $("#MSOZoneCell_WebPartWPQ6").css('background-color', 'blue');
});
</script>

Résultat

Le background-color bleu est appliqué uniquement sur la WebPart avec le ID MSOZoneCell_WebPartWPQ6 :

WebPartByID_After

Points à faire attention avec cette technique

  1. Assurez-vous d’ajuster votre code si une WebPart est déplacé,  ajouté ou supprimé

Que se passe-t’il si on déplace une WebPart ou qu’on en ajoute une nouvelle?

Le ID de la WebPart va tout simplement changer.

Avant la suppression d’une WebPart (2 WebParts) :

WebPartByTitle_HTMLElement_AvantDelete

WebPart 1 : WebPartWPQ4_ChromeTitle

 

Après la suppression d’une WebPart (1 WebPart) :

WebPartByTitle_HTMLElement_ApresDelete

WebPart 1 : WebPartWPQ2_ChromeTitle 

Voici un autre exemple qui utilise cette technique.

Conclusion

Les deux techniques présentés dans cet article peuvent vous permettre facilement d’appliquer des personnalisations sur un élément HTML d’une WebPart. La première technique utilisant le titre de la WebPart n’est pas sans faille si deux WebParts se retrouvent avec le même nom. Pour ce qui en est d’utiliser le ID de l’élément HTML de la WebPart, il faut faire attention si on déplace, supprime ou ajoute une WebPart dans la page car les ID sont susceptible de changer et ainsi de rendre votre code non fonctionnel. Ces techniques peuvent vous permettre d’appliquer des modifications rapidement lorsque ce n’est pas possible de le faire avec du CSS ou que c’est trop complexe avec du Client Side Rendering.

Utiliser les Patterns and Practices et le modèle des Apps pour déployer vos sites dans SharePoint Online ou on-premise

Introduction

Créer efficacement des sites personnalisées pour répondre aux besoins de nos clients c’est une des raisons de l’existence de SharePoint. Microsoft se concentre de plus en plus vers des déploiements dans le cloud, alors nous aurons inévitablement besoin d’avoir un modèle de déploiement qui est viable et facilement réalisable à la fois sur O365 et on-premise.

Dans cette optique, je me suis penché sérieusement sur la question afin de mieux comprendre les différentes techniques de déploiement de sites ainsi que leurs avantages et inconvénients.

Le déploiement de sites personnalisés dans SharePoint a évolué au cours des dernières versions et avec l’arrivé du modèle des Apps, il est évident qu’une façon de faire uniformisé s’impose. Une des techniques consiste à utiliser le modèle de Apps pour déployer vos sites.

Différentes techniques de déploiement de sites personnalisés

Avant d’entrer dans le vif du sujet, voyons un bref résumé des techniques qui s’offrent à nous pour effectuer le déploiement :

  • Modèles Web (Web Templates)
    • Seulement disponible au niveau de la collection de site dans O365
    • Difficile à maintenir à jour
  • Définitions de site (Site Definitions)
    • Non supporté dans O365
    • Beaucoup d’impact lors des mises à jours
  • Solutions de ferme personnalisées
    • Non supporté dans O365
    • Complexe à mettre en place et à maintenir
  • Création de structure de site avec Powershell
    • Complexe à mettre en place et à maintenir
  • Agrafage de fonctionnalités (Feature stappling)
    • Seulement disponible au niveau de la collection de site dans O365

Changement d’orientation de la part de Microsoft

Les modèles web (Web Templates) ont été, jusqu’à dernièrement, un excellent mécanisme fournis par Microsoft pour créer des centaines de sites d’équipes avec leurs propres personnalisations (bibliothèques, vues, WebPart, etc…) en addition à un modèle de site « out of the box ».

Cependant, lors d’une conférence au TechEd Europe 2014 (30 Octobre) Microsoft a annoncé ceci :

  • Éviter d’utiliser des définitions de site et des modèles Web
  • Éviter les pages maîtres personnalisées
  • Éviter d’utiliser le code XML déclaratif

siteProvisioning_NoSiteDef

Lien vers le vidéo

La raison derrière ce changement est, à mon avis, que Microsoft tente de faire d’Office 365 une plate-forme de développement qui n’est pas onéreuse à faire fonctionner et en même temps de fournir un service qui n’est pas trop complexe.

Maintenant, si on utilise des modèles Web nous ne bénéficieront pas des derniers changements pour chaque mise à jour de Microsoft. On parle ici d’une « taxe de personnalisation » que vous devez payer si vous utilisez des modèles Web.

Réaction face à ce changement d’orientation

J’évite volontairement de vous parler de l’orientation qui consiste à éviter les pages maître personnalisés et le code XML déclaratif car je ne suis pas complètement en accord avec ce changement. Je partage l’opinion de Chris O’Brien, il y aura toujours place à la personnalisation dans un site Intranet et le code XML déclaratif est encore supporté à 100% dans O365!

Tout comme nous n’avons pas immédiatement abandonné les solutions de fermes lors de l’arrivé du modèle des Apps, les modèles Web continueront sans doute d’être utilisé pour plusieurs scénarios pour où vous ne voudriez PAS de ces derniers changements provenant des mises à jour de Microsoft.

Cependant, je considère que pour les site d’équipe ainsi que pour les sites de projets, il n’est plus favorable d’utiliser un modèle Web et il serait plus approprié d’utiliser le modèle des Apps pour déployer vos sites afin d’éviter cette taxe de personnalisation!

Le modèle des Apps pour déployer vos sites

Le diagramme suivant illustre le déploiement de collections de site en utilisant le modèle des Apps ainsi qu’une approche asynchrone :

 

siteProvisioning_Remote

  1. Lorsqu’un utilisateur a besoin d’une nouvelle collection de site, il remplis les métadonnées requises dans le formulaire de création
  2. L’information est stockée dans un endroit centralisé
  3. Si requis, on peut y associer des flux d’approbation
  4. Les requêtes approuvées sont traitées par une tâche planifié distante. Ex : Une tâche planifié hébergé dans un serveur applicatif qui lance un script PowerShell ou une application console
  5. Le déploiement du site est réalisé en fonction des métadonnées fournis en utilisant le CSOM
  6. Une notification est envoyé à l’utilisateur pour lui informer de la création de la collection de site.

Le principe de cette technique consiste à sortir le code permettant d’effectuer le déploiement à l’extérieur de SharePoint afin d’avoir un faible couplage avec la plateforme. Cette technique fonctionne dans O365 ainsi qu’on-premise. Un des avantages est qu’il n’est pas nécessaire de déployer de fonctionnalités dans la ferme pour effectuer le déploiement. En plus de cela, il n’y a plus de dépendances avec des packages de solutions à l’intérieur des bases de données de contenu alors on pourrait uniquement restaurer celles-ci en cas de disaster recovery.

Fonctionnement technique

  1. Aller sur le site GitHub de OfficeDevPnp à l’adresse https://github.com/OfficeDev/PnP
  2. Cliquer sur le bouton « Download zip » et choisir un emplacement
  3. Extraire le Zip et aller dans le répertoire PnP-master\PnP-master\Samples\Provisioning.OnPrem.Async
  4. Ouvrir Provisioning.OnPrem.Async.Console.sln avec Visual Studio
  5. Vous retrouverez la méthode principale ProcessSiteCreationRequest
[...]

private static string ProcessSiteCreationRequest(ClientContext ctx, ListItem listItem)
{

 //get the base tenant admin urls
 string tenantStr = ConfigurationManager.AppSettings["SiteCollectionRequests_SiteUrl"];

 // Resolve root site collection URL from host web. We assume that this has been set as the "TenantAdminSite"
 string rootSiteUrl = tenantStr.Substring(0, 8 + tenantStr.Substring(8).IndexOf("/"));

 //Resolve URL for the new site collection
 var webUrl = string.Format("{0}/sites/{1}", rootSiteUrl, listItem["SiteUrl"].ToString());
 var tenantAdminUri = new Uri(rootSiteUrl);
 string realm = TokenHelper.GetRealmFromTargetUrl(tenantAdminUri);
 var token = TokenHelper.GetAppOnlyAccessToken(TokenHelper.SharePointPrincipal, tenantAdminUri.Authority, realm).AccessToken;
 using (var adminContext = TokenHelper.GetClientContextWithAccessToken(tenantAdminUri.ToString(), token))
 {
 // Set the time out as high as possible
 adminContext.RequestTimeout = int.MaxValue;

 var tenant = new Tenant(adminContext);
 var properties = new SiteCreationProperties()
 {
 Url = webUrl,
 Owner = listItem["AdminAccount"].ToString(),
 Title = listItem["Title"].ToString(),
 Template = listItem["Template"].ToString(),
 };

 //start the SPO operation to create the site
 SpoOperation op = tenant.CreateSite(properties);
 adminContext.Load(op, i => i.IsComplete);
 adminContext.RequestTimeout = int.MaxValue;
 adminContext.ExecuteQuery();
 }

 // Do some branding for the new site
 SetThemeToNewSite(webUrl);

 return webUrl;
}

[...]

Afin d’uniformiser la manière de faire entre O365 et on-premise, vous remarquerez qu’on utilise la même notion de « Tenant« .

Une fois le code déployé, vous aurez un interface comme celui-ci pour saisir l’information concernant la collection de site à créer :

siteProvisioning_UI

Cette information sera ensuite sauvegardée dans une liste :

siteProvisioning_List

Finalement, l’application console que vous aurez lancé manuellement ou via une tâche planifié se chargera de créer la collection de site à partir de cette liste.

Pour plus d’information consulter ce vidéo qui présente en détail le fonctionnement du code et du processus.

Office Development Patterns & Practices (PnP)

En plus de fournir le code pour déployer des sites, le site d’Office PnP présente une série d’exemples et de scénarios sur les bonnes pratiques qui utilise le modèle des Apps.

Les exemples et scénarios utilisent les méthodes qui sont recommandées par l’équipe Office 365 et la majorité s’applique à :

  • Office 365 Multi Tenant (MT)
  • Office 365 Dedicated (D)
  • SharePoint 2013 on-premises

Voici quelque références pour les différents sites relié à Office PnP :

Conclusion

Le modèle des Apps pour déployer vos sites peut être la bonne solution dans une situation et peut ne pas l’être dans une autre. Le plus important est d’avoir une bonne compréhension des diverses techniques pour ensuite prendre une décision éclairé selon vos besoins. Le choix de votre technique de déploiement de site aura un impacte à long terme sur vos coûts de mise en place et de maintenance. J’aime bien la technique asynchrone proposé par Office PnP car il est possible de l’utiliser dans O365 ainsi que on-premise et de plus, l’utilisateur n’a pas besoin d’attendre dans son navigateur car il reçoit une notification à la fin du traitement.

Qu’est-ce que vous pensez de cette technique de déploiement de sites ?

Références

Building a async provisioning model par Office PnP

Office365 Developer Patterns and Practices par Vesa Juvonen

Provisioning site collections using SP App model in on-premises with just CSOM par Vesa Juvonen

Site provisioning techniques with SharePoint apps par Bert Jansen et Vesa Juvonen

Transforming Your SharePoint Full Trust Code to the SharePoint App Model  par Vesa Juvonen et Steve Walker

Developing for Office 365 – thoughts on use of custom master pages and web templates par Vesa Juvonen

Site provisioning techniques and remote provisioning in SharePoint 2013 par Chris O’Brien

L’effort de Microsoft au sujet de l’extensibilité d’Office 365 par Sébastien Levert

Personnaliser vos sites SharePoint 2013 avec le Client Side Rendering (CSR) et le JS Link

Introduction

Effectuer des personnalisations dans l’interface utilisateur de SharePoint a toujours été un défi pour bien des développeurs. Dans les versions 2007 et 2010, on utilisait majoritairement le XSLT pour faire des personnalisations bien que c’était relativement complexe à s’y familiariser. D’autres utilisaient des WebParts de type Éditeur de contenu pour effectuer des modifications JavaScript directement dans une page et dans ce cas la difficulté est de trouver une « poignée » pour appliquer les modifications sur un élément précis. La fonctionnalité JSLink est arrivé dans SharePoint 2013 pour simplifier la vie aux développeurs en leur permettant de faire des personnalisations avec des langages qu’ils connaissent déjà : HTML, JavaScript et CSS. Grâce à cette fonctionnalité, on peut contrôler via un fichier JavaScript le rendu visuelle des éléments suivant :

  • Champ
  • Élément
  • Formulaire de liste
  • Vue
  • WebPart

XSL/XSLT vs Client Side Rendering

Le tableau ici-bas résume bien les différences entre les deux fonctionnalités :

XSL/XSLT Client Side Rendering
Performance
  • Plus rapide lorsqu’il y a un grand nombre d’éléments à afficher.
  • Aucune charge additionnel dans le navigateur.
  • Plus lent lorsqu’il y a un grand nombre d’éléments à afficher.
  • La vitesse du navigateur client peut avoir un impact sur la performance.
  • Réduit la charge du serveur.
Complexité
  • Langage complexe à apprendre initialement.
  • Difficile à déboguer (Souvent par essaie-erreur).
  • Temps de développement plus grand.
  • Facile de déboguer du JavaScript, HTML et CSS avec les navigateurs modernes.
  • Temps de développement réduit.
Flexibilité
  • Limité aux fonctions XSLT 1.0 pour SharePoint 2010 et 2013.
  • Limité pour faire un rendu visuel complexe.
  • Très efficace pour faire des opérations coûteuse (ex : Count dans une vue).
  • Permet de cibler les personnalisations sur une vue, un champ, une WebPart, etc…
  • Possibilité d’utiliser des plugins (ex : JQuery).
  • Très adapté pour faire un rendu visuel complexe.
  • Inefficace pour faire des opérations coûteuses (ex : Count dans une vue).
Compatibilité
  • Indépendant du navigateur.
  • Supporté dans SP2010 et SP2013.
  • Fonctionne même si le JavaScript est désactivé dans le navigateur.
  • Génère du contenu statique qui est bien interprété par les robots d’indexation.
  • Adapté pour des sites publiques.
  • Dépendant du navigateur.
  • Supporté dans SP2010 et SP2013
  • Requiert que le JavaScript soir activé dans le navigateur.
  • Génère du contenu dynamique qui n’est pas bien interprété par les robots d’indexation.
  • Non-adapté pour des sites publiques.

Comment configurer le JSLink?

Pour référencer vos fichiers dans la propriété JSLink vous pouvez spécifier un jeton de remplacement (Token) ainsi qu’un séparateur (|) si vous avez plusieurs fichiers.

Les jetons disponibles sont les suivants :

  • ~site : correspond au URL du site Web courant.
  • ~sitecollection : correspond au URL de la collection de site parent du site Web courant.
  • ~layouts : correspond au dossier « _layouts/15 » de la web application.
  • ~sitelayouts – correspond au dossier layouts du site courant (ex : site/monsite/monsoussite/_layouts/15).
  • ~sitecollectionlayouts : correspond au dossier layouts de la collection de site courante (Ex:  /sites/monsite/_layouts/15).
  • ~sitelayouts – correspond au dossier layouts du site courant (Ex: /site/monsite/monsoussite/_layouts/15).

En utilisant ces jetons avec le séparateur on peut donc référencer plusieurs fichiers de scripts dans le JSLink.

Ex : ~site/style library/­monScript1.js|~sitecollection/style library/monScript2.js.

On peut spécifier la propriété JSLink en utilisant ces méthodes :

  • PowerShell
  • Code serveur C#/VB en utilisant l’API
  • Code serveur déclaratif XML
  • Interface utilisateur

Pour simplifier ce billet, je vous propose la méthode utilisant l’interface utilisateur :

jslink_link

Pour plus de détail sur les autres méthodes permettant de spécifier la propriété JSLink, je vous recommande le billet de Tobias Zimmergren.

Fonctionnement technique

À l’intérieur de ces fichiers, le JavaScript est utilisé pour « overrider » :

  • la manière dont les données sont affichés
  • le contenu à afficher

Voici un exemple de code permettant de modifier la valeur d’un champ dans une vue :

// Créer un namespace pour la fonction afin d'éviter des collisions avec les autres fonctions
var sharepointerie = sharepointerie || {};

(sharepointerie.CustomizeFieldRendering = function () {

// Initialise les variables pour overrider les objets
 var overrideCtx = {};
 overrideCtx.Templates = {};

 /*
 * Force la valeur "SharePointerie" dans le titre de tous les éléments.
 */
 overrideCtx.Templates.Fields = {
 'LinkTitle': { 'View' : 'SharePointerie' }
 };

 /*
 * Enregistre l'override du template.
 */
 SPClientTemplates.TemplateManager.RegisterTemplateOverrides(overrideCtx);
})();

Techniquement, on construit un objet qui sera ensuite utilisé afin d’enregistrer l’override du template. L’objet « overrideCtx » peut ensuite interagir avec ces propriétés :

  • View
  • Header
  • Body
  • Footer
  • Group
  • Item
  • Fields
  • OnPreRender
  • OnPostRender

Dans l’exemple on utilise la propriété Fields avec ce format :

overrideCtx.Templates.Fields = {NomDuChamp: {Porté : Override}}

  • NomDuChamp : Le nom interne du champ que l’on veut overrider.
  • Porté : Les choix possibles « View », »DisplayForm », »EditForm », et « NewForm » permettent de spécifier l’endroit ou l’on veut faire l’override.
  • Override : Contient une chaîne HTML ou une fonction à exécuter à l’intérieur de tags « <#= #> ».

Résultat (avant l’ajout du CSR) :

jslink_technique_avant

Résultat (après l’ajout du CSR) :

jslink_technique_apres

Des exemples concrets

1) Limiter la taille du texte dans le champ « Corps » d’une liste d’annonces

Cet exemple de code, réalisé par Muawiyah Shannak, permet d’afficher un résumé du corps d’une annonce et d’afficher la version intégrale dans une infobulle.

Pour déployer la solution :

  1. Télécharger le fichier CSR.
  2. Ajouter le fichier sous /Style Library/.
  3. Créer une liste de type Annonces
  4. Créer ou modifier une vue afin d’afficher le champ Corps.
  5. Modifier la page contenant la List View WebPart.
  6. Aller dans les propriétés de la WebPart et ajouter le fichier JSLink (~sitecollection/Style%20Library/SubstringLongText.js) dans la propriété Lien Js (JSLink) sous l’onglet Divers
  7. Cliquer sur Appliquer et quitter l’édition de la page.

Résultat (avant l’ajout du CSR) :

jslink_limite_texte_avant

Résultat (après l’ajout du CSR) :

jslink_limite_texte_apres

2) Ajouter des onglets dans un formulaire

Cet exemple de code, réalisé par Muawiyah Shannak, permet d’utiliser JQuery Tabs pour afficher des onglets dans un formulaire.

Pour déployer la solution :

  1. Télécharger le fichier CSR.
  2. Ajouter le fichier sous /Style Library/.
  3. Créer une liste personnalisé
  4. Ajouter des colonnes à la liste
  5. Modifier l’objet tabsObj selon vos besoin.
  6. Créer une nouvelle vue contenant le champ Lien
  7. Modifier les pages « newform » et « editform » de la liste
  8. Aller dans les propriétés de la WebPart et ajouter le fichier JSLink (~sitecollection/Style%20Library/Tabs.js) dans la propriété Lien Js (JSLink) sous l’onglet Divers
  9. Cliquer sur Appliquer et quitter l’édition de la page.

Résultat (avant l’ajout du CSR) :

jslink_tabs_avant

Résultat (après l’ajout du CSR) :

jslink_tabs_apres

Points à faire attention

  • Il peut y avoir des conflits avec le CSR si vous utilisez la fonctionnalité « Minimal download strategy ». Pour corriger ce problème consulter ce billet.
  • Le code de vos fonctions CSR ne devrait pas être défini dans le Namespace globale.
  • Le nom interne des colonnes est utilisé dans le code JavaScript des fonctions CSR alors favoriser des noms de colonnes sans espaces et sans caractères accentués.
  • Le CSR, contrairement au XSLT, dépend du navigateur alors assurez-vous de tester dans plusieurs navigateurs.

Conclusion

Pour conclure, le CSR comporte plusieurs avantages au niveau de la flexibilité et de la complexité comparativement au XSLT. Cependant, il vient aussi avec certaines lacunes en lien avec la performance et la compatibilité. Il y a donc énormément de potentiel à utiliser le CSR pour vos personnalisations (Validations, AJAX, etc…) mais comme il y a des limitations, je crois que le XSLT va continuer d’exister en parallèle. Je vous invite donc à l’essayer et à me donner vos commentaires!

Références

Voici quelques liens pour approfondir vos connaissance sur le CSR / JSLink.

Exemples de codes CSR réalisé par Muawiyah Shannak

Perfomance de l’utilisation du CSR via le JSLink

5 faits concernant l’utilisation du JSLink

Comment : personnaliser un type de champ à l’aide de rendu côté client

Comment : personnaliser le mode Liste dans les apps pour SharePoint à l’aide du rendu côté client

Introduction au CSR par Ross Bradbrook

Bon point de départ sur l’utilisation de l’API CSR

Conversion de solutions de ferme SharePoint 2010 vers SharePoint 2013

De retour sur mon blog après des vacances bien mérité. On m’attendait de pied ferme pour évaluer avec d’autres collaborateurs la conversion de plusieurs solutions de ferme SharePoint 2010 vers SharePoint 2013. Je vais donc tenter, dans ce billet, de vous aider à identifier et à corriger plus rapidement d’éventuelles problématiques de conversion.

Conversion des Requête de contenu (Content Query WebPart) SharePoint 2010 vers SharePoint 2013

Dans les composantes contenant des Requêtes de contenu (Content Query WebPart), nous sommes tombés sur une SharePointerie lors de la conversion. À première vue, après avoir effectué la conversion vers SharePoint 2013 de cette composante avec Visual Studio 2013, le code compile!

conversion_sp2013_Upgrade

On est prêt pour aller en PROD ? Pas tout à fait! J’ai alors déployé la composante et lorsque j’ai essayé de la tester alors j’ai obtenu un message d’erreur lors de l’affichage de la WebPart.

Dans la plupart des Requête de contenu à convertir vers 2013, l’aspect visuel a été modifié afin de fournir une expérience utilisateur plus riche. Pour s’y prendre, le lien vers les fichiers XSL par défaut a été modifié dans le XML du fichier *.webpart.

Exemple d’un fichier *.webpart SharePoint 2010

<webParts>
<webPart xmlns="http://schemas.microsoft.com/WebPart/v3">
 <metaData>
   ...
 </metaData>
 <data>
 <properties>
   <...>
   <property name="MainXslLink" type="string">/Style Library/XSL Style Sheets/POC.SHP.WebPart.Main.xsl</property>
   <property name="ItemXslLink" type="string">/Style Library/XSL Style Sheets/POC.SHP.WebPart.Item.xsl</property>
   <...>
 </properties>
 </data>
 </webPart>
</webParts>

J’ai alors tenté de retirer les liens XSL afin d’utiliser l’affichage par défaut de la Requête de contenu (Content Query WebPart) et le tout s’est affiché mais sans inclure l’aspect visuel personnalisé. On s’est donc dit que le URL vers le fichier XSL n’était pas bon. Je me suis alors rappelé que dans SharePoint 2013, il faut maintenant spécifier dans le URL le dossier /15/ (Aussi appelé Hive) avant le /Style Library/.

Exemple d’un fichier *.webpart corrigé pour SharePoint 2013

<webParts>
<webPart xmlns="http://schemas.microsoft.com/WebPart/v3">
 <metaData>
   ...
 </metaData>
 <data>
 <properties>
   <...>
   <property name="MainXslLink" type="string">/15/Style Library/XSL Style Sheets/POC.SHP.WebPart.Main.xsl</property>
   <property name="ItemXslLink" type="string">/15/Style Library/XSL Style Sheets/POC.SHP.WebPart.Item.xsl</property>
   <...>
 </properties>
 </data>
 </webPart>
</webParts>

Pourquoi est-ce ainsi?

Dans SharePoint 2013, il est possible d’installer à la fois des solutions SharePoint 2010 et 2013. Ceci est supporté par le fait qu’il y a deux dossiers virtuel, un pour le SP2010 (14) et l’autre pour les SP2013 (15).

Si votre code SharePoint 2010 contient des références vers /_layouts/ ou /Style Library/, il vous faudra les mettre à jour respectivement pour /_layouts/15/ ou  /15/Style Library/ afin que le chemin soit résolus correctement lors de l’utilisation de la solution dans SharePoint 2013.

Le tableau ici-bas présente les différences des chemins :

SharePoint 2010 SharePoint 2013
/Style Library/scripts/jquery.min.js /15/Style Library/scripts/jquery.min.js
/_layouts/images/test.jpg /_layouts/15/images/test.jpg

Conversion des Visual WebParts SharePoint 2010

Lors de la conversion des composantes de ferme de type Visual Web Part nous sommes tombé sur une autre SharePointerie. Cependant, lorsqu’on connait la recette vue précédemment ça devient un peu plus évident.

Lorsque j’ai ajouté la Visual WebPart dans ma page, ça à tout simplement fait exploser la page et SharePoint m’a proposé la page de maintenance afin de supprimer la WebPart en erreur. C’est très gentil de sa part, mais ce n’est pas très utile pour diagnostiquer un problème. Alors, j’ai décidé de m’attacher au code et de déboguer la méthode CreateChildControls qui est lancé au tout début lors de l’affichage de la Visual WebPart.

C’est alors que j’ai eu ce message d’exception : The file ‘/_controltempates/…/…UserControl.ascx’ does not exist.

conversion_sp2013_debug_visualwebpart

Pourtant mon fichier *.ascx est bien déployé dans le dossier :

conversion_sp2013_15_visualwebpart

J’ai alors appliqué la même recette que vue précédemment en ajoutant le /15/ dans le URL tout de suite après le /_controltemplates/ et bien sûr ça a bien fonctionné :

conversion_sp2013_debug_passed_visualwebpart

Il faut comprendre que le commentaire dans le haut de la constante _ascxPath indique : Visual studio might automatically update this path when you change the Visual WebPart.

Cependant, Visual Studio ne l’a pas fait lors de la conversion de SharePoint 2010 vers SharePoint 2013 ! J’ai ouvert un billet sur le site de Visual Studio User Voice alors n’hésiter pas à aller voter pour celui-ci. Qui sais, un jour Visual Studio sera peut-être amélioré et cette SharePointerie n’existera plus!

Conclusion

En conclusion, si votre code SharePoint 2010 contient des références vers /_layouts/, /_controltemplates/ ou /Style Library/ vous aurez à modifier vos URL pour que ça fonctionne dans SharePoint 2013. Il ne s’agit pas d’une liste exhaustive des problématiques de conversion que l’on peut retrouver mais c’est un bon point de départ. De plus, lorsque l’on connait la recette pour corriger les problèmes c’est beaucoup plus simple et on perd beaucoup moins de temps.

Avez-vous effectué des conversions SharePoint 2010 vers SharePoint 2013 ? Est-ce que vous avez rencontré des SharePointeries? N’hésitez pas à commenter pour lancer la discussion!

Comment créer un service Web WCF (REST) personnalisé dans SharePoint 2013

Récemment, un collègue m’a demandé comment faire pour exécuter du code serveur dans un bouton du ruban. Je lui avait alors répondu : Le plus simple c’est de te faire une page aspx avec une WebPart contenant du code serveur. C’est comme si tu appelais un service web mais au lieu de cela c’est une page aspx avec des paramètres dans le querystring.

Ex : Lors du clique sur un bouton du ruban on appel une page avec des paramètres pour effectuer la modification d’un élément. Assumons que la page se nomme ModifierElement.aspx, alors il sera possible d’y accéder seulement à partir de sont chemin fixe : http://serveur/sites/site/web/pages/ModifierElement.aspx?Id=1

Dans le code du ruban (CustomAction), on peut seulement utiliser du code JavaScript (CSOM) et pour utiliser du code serveur on doit invoquer un événement via le bouton. Ces deux dernières méthodes comportent un certain niveau de complexité ainsi que des limitations.

L’approche de la page aspx est souvent utilisé pour sauver du temps lors du développement. Cependant, après quelques recherches je suis tombé sur une meilleure façon d’exécuter du code serveur en utilisant un Service Web WCF (REST) personnalisé hébergé dans SharePoint.

Comment faire un service Web (WCF) personnalisé ?

Les services Web natif de SharePoint peuvent être accédés à partir du chemin _vti_bin.  Lorsque l’on va développer notre service Web personnalisé il sera accessible via ce même chemin.

Ex : Assumons que le service Web personnalisé se nomme Service.svc alors il sera possible d’y accéder, entre autres, à partir des chemins suivants :

* C’est un avantage comparativement à la méthode de la page contenant une WebPart 

Étape 1 – Créer un projet Visual Studio 2012

  • Créer un nouveau projet à partir du modèle « SharePoint 2013 – Empty Project »
  • Nommer le projet « POC.SharePoint.WebServiceHelloWord »
  • Sélectionner « Deploy as a farm solution »

Étape 2 – Ajouter les références

  • Ajouter les références suivantes dans votre projet : Microsoft.SharePoint.Client.ServerRuntime 15.0.0.0 et System.ServiceModel 4.0.0.0

referenceWcf

Étape 3 – Ajouter un SharePoint Mapped Folder (ISAPI)

Le dossier ISAPI est le dossier spécial dans la « hive » SharePoint où tous les services WCF doivent être déployés. Ce dossier est associé au chemin « _vti_bin » dans IIS et ainsi les services déployés dans ce dossier peuvent être accédé par ce URL : http://serveur/_vti_bin/{NomService}.svc

Pour ajouter un nouveau Mapped Folder on sélectionne le projet et ensuitie on clique de droit -> ajouter -> SharePoint Mapped Folder :

mappedFolderWcf

Ensuite sélectionner le dossier ISAPI :

isapiWcf

C’est une bonne pratique de se créer un sous dossier spécifique à votre projet e.g.: « WebServiceHelloWord » .

Étape 4 – Définir le Service, le Contract et l’Interface

Dans le Mapped Folder « ISAPI » ajouter les fichiers suivants :

  • un fichier texte nommé « Service.svc » (Fichier du service lui même qui sera déployé dans le hive de SharePoint et qui sera appelé dans IIS)
  • un fichier Interface nommé « IService.cs » (Fichier contenant le contrat d’opération du service)
  • un fichier Classe nommé « Service.svc.cs » (Fichier contenant l’implémentation de l’interface du service)

Étape 5 – Définir le contenu du Service, du Contract et de l’Interface

Ouvrez le fichier Service.svc.cs et y insérer le code suivant :

using Microsoft.SharePoint.Client.Services;
using System.ServiceModel.Activation;

namespace POC.SharePoint.WebServiceHelloWord.ISAPI.WebServiceHelloWord
{
 [BasicHttpBindingServiceMetadataExchangeEndpoint]
 [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
 public class Service : IService
 {
 public string HelloWorld()
 {
 return "Hello World de mon service WCF dans SharePoint 2013";
 }
 }
}

Ouvrir le fichier IService.cs et y insérer le code suivant :

using System.ServiceModel;

namespace POC.SharePoint.WebServiceHelloWord.ISAPI.WebServiceHelloWord
{
 [ServiceContract]
 public interface IService
 {
 [OperationContract]
 string HelloWorld();
 }
}

Ouvrir le fichier Service.svc et y insérer le code suivant :

<%@ ServiceHost Language="C#" Debug="true"
 Service="POC.SharePoint.WebServiceHelloWord.ISAPI.WebServiceHelloWord.Service, $SharePoint.Project.AssemblyFullName$"
 CodeBehind="Service.svc.cs"
 Factory="Microsoft.SharePoint.Client.Services.MultipleBaseAddressBasicHttpBindingServiceHostFactory,
 Microsoft.SharePoint.Client.ServerRuntime,
 Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>

Étape 6 – Permettre à VS d’effectuer le remplacement des tokens dans un fichier *.svc

Dans le code du dernier fichier Service.svc qu’on a ajouté il y a une ligne contenant un jeton de remplacement (Token) : $SharePoint.Project.AssemblyFullName$

Si on ne fait rien on aura une erreur lors de la compilation. Il y a plusieurs façon de s’y prendre mais à mon avis la façon la plus élégante est de permettre à VS d’effectuer le remplacement de ces tokens pour les fichiers « *.svc ». J’appel cette technique la passe du coyote qui tousse!

Pour s’y faire, on doit effectuer un « Unload Projet » et ensuite un « Edit {NomDuProjet}.csproj ».

Ensuite trouver la ligne suivante :

<SandboxedSolution>False</SandboxedSolution>

Après cette ligne ajouter celle-ci :

<TokenReplacementFileExtensions>svc</TokenReplacementFileExtensions>

Sauvegarder le fichier et faire un « Reload project ».

Étape 7 – Compiler, déployer et tester le service WCF (REST) personnalisé

Une fois que vous aurez compilé et déployé votre service, vous pourrez le tester directement par son URL : http://serveur/_vti_bin/WebServiceHelloWord/Service.svc

testWcf

Cliquez ici pour télécharger le code source

Autres cas d’utilisations

Bien évidemment, le message retourné par notre fonction « HelloWorld » ne nous apporte rien de concret mais voici quelques exemples d’utilisation que l’on peut faire avec un service Web WCF (REST) personnalisé dans SharePoint :

  • Modifier les métadonnées d’un document (Ex: Ajouter un préfixe)
  • Exporter un rapport personnalisé en PDF
  • Modifier la sécurité d’un élément
  • Envoyer un document en pièce jointe d’un courriel
  • etc…

Conclusion

Utiliser un service Web WCF (REST) personnalisé dans SharePoint comporte plusieurs avantages comparativement à utiliser une page « aspx » avec une WebPart. D’une part, le service est accessible plus facilement via « _vti_bin » dans tout les sites et lorsqu’on utilise un service web WCF on envoie seulement le XML nécessaire au lieu d’envoyer tout le contenu HTML d’une page « aspx ».