Rendre un calendrier SharePoint 2010 consultable pour iPhone/iPad ou autre smartphone/tablet

Pendant vos projets SharePoint, vos utilisateurs vont ont certainement fait part qu’ils souhaitaient synchroniser un calendrier spécifique et particulièrement important avec leur iPhone ou leur iPad (que ce soit en interne ou en extérieur). Néanmoins, L’iPhone ne sait pas le faire pas naturellement : rien de plus simple qu’une publication au format iCalendar pour palier au problème !

Pour faire face à cette situation, direction Visual Studio 2010 (avec Nuget installé) ou 2012 (mieux !) :

  1. Créez un nouveau projet SharePoint vide
  2. Ajoutez le package DDay.iCal à votre projet (que vous pouvez trouver ici :http://nuget.org/packages/DDay.iCal) qui va nous permettre de générer simplement la sortie iCal
  3. Créez un fichier handler .ashx et copier (en l’adaptant à votre environnement) suivant :

<%@ Assembly Name= »Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c » %>
<%@ Assembly Name= »Contoso.iCal, Version=1.0.0.0, Culture=neutral, PublicKeyToken=5d957034fc68cb87″ %>
<%@ WebHandler Language= »C# » Class= »Consoto.iCalPublisher.Agenda » %>

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Web;
using Noumea.iCal;
using Microsoft.SharePoint;

namespace Contoso.iCalPublisher
{
    public class Agenda : IHttpHandler
    {
        public bool IsReusable
        {
            get { return false; }
        }

        public void ProcessRequest(HttpContext context)
        {
            iCalCalendar cal = new iCalCalendar();

            using (SPSite site = new SPSite(« http://demo.contoso.nc/managers/ »))
            {
                using (SPWeb web = site.RootWeb)
                {
                    // Récupération du calendrier à publier en iCal
                    SPList calManager = web.Lists.TryGetList(« Agenda Managers »);
                    if (calManager != null)
                    {
                        // Filtrage par rapport à un type de contenu et ne récupère que les 15 derniers jours d’évènements antérieurs à la date du jour
                        SPQuery query = new SPQuery();
                        query.Query = string.Concat(
                                « <Where><And><Eq><FieldRef Name=’ContentType’/><Value Type=’Text’>Evènement Manager</Value></Eq><Geq><FieldRef Name=’EndDate’ /><Value Type=’DateTime’><Today OffsetDays=’-15′ /></Value></Geq></And></Where> »);
                        SPListItemCollection listItems = calManager.GetItems(query);
                        // On exporte tous les évènements récupérés en iCal
                        foreach (SPListItem item in listItems)
                        {
                            iCalEvent evt = new iCalEvent();
                            evt.DateBegin = DateTime.Parse(item[« EventDate »].ToString());
                            evt.DateEnd = DateTime.Parse(item[« EndDate »].ToString());
                            evt.Description = item[« Title »].ToString();
                            evt.Location = item[« Location »] != null ? item[« Location »].ToString() : «  »;
                            cal.Events.Add(evt);
                        }
                    }
                }
            }

            byte[] arrBytData = cal.Serialize();
            context.Response.Clear();
            context.Response.ContentType = « text/plain »;
            // Pour éviter le cache
            context.Response.AppendHeader(« Pragma », « no-cache »);
            context.Response.AppendHeader(« Expires », « Sat, 05 Jul 2010 05:00:00 GMT »);
            context.Response.AppendHeader(« Content-Disposition », « attachment; filename=CalendrierManager.ics »);
            context.Response.AppendHeader(« Content-Length », arrBytData.Length.ToString());
            context.Response.ContentType = « text/calendar »;
            context.Response.BinaryWrite(arrBytData);
            context.Response.Flush();
            context.Response.End();
            context.Response.Close();
        }
    }
}

That’s all folks ! Bien évidemment la sécurité fonctionne à merveille (authentification requise).

Créer une webpart de génération de sommaire automatique des titres d’une page de publication pour le publishing de SharePoint 2010

Voici un petit bout de code rapide pour réaliser un sommaire des titres (H1) dans une page de publication assez longue, avec un retour vers le sommaire sous le titre :

var WebPart_SommaireAncres_Variable_Sommaire_CodeRetourSommaire = « <div class=’WebPart_Sommaire_RetourSommaire’><a class=’WebPart_Sommaire_RetourSommaire_a’ href=’#Sommaire’>Retour Sommaire</a><img class=’WebPart_Sommaire_RetourSommaire_img’ src=’/_layouts/DemoCorp/images/WebPart_Sommaire_Fleche-Haut.png’ /></div> »

jQuery(document).ready(function () {

    var divMainResize = document.getElementById(‘MainResize’);

    /* Ajout des ancres sur les H1 */
    var divh1Array = divMainResize.getElementsByTagName(‘h1’);
    for (var i = 0; i < divh1Array.length; i++) {
        // Ajoute l’id qui permet au menu d’identifier le titre
        jQuery(divh1ArrayIdea).attr(« id », « sommaireIndex » + i);
        // Ajoute le lien de retour vers le sommaire
        jQuery(divh1ArrayIdea).after(WebPart_SommaireAncres_Variable_Sommaire_CodeRetourSommaire);
    }

    /* Ajout du sommaire */
    var elSommaire = jQuery(« <a name=’Sommaire’></a> »);
    jQuery(divMainResize).prepend(elSommaire);
    // Génération du sommaire
    var elContenuSommaire = jQuery(« <div id=’WebPart_Sommaire’></div> »).appendTo(elSommaire);
    jQuery(« <h1 class=’WebPart_Sommaire_h1′>Sommaire</h1> »).appendTo(jQuery(elContenuSommaire));
    var elSommaireListe = jQuery(« <ul class=’WebPart_Sommaire_Liste_Lien_ul’></u> »);
    jQuery(elSommaireListe).appendTo(jQuery(elContenuSommaire));
    for (var i = 1; i < divh1Array.length; i++) { // Début à 1 car nous ajoutons un H1 avec le titre du sommaire
        var contenuLi = jQuery(« <li class=’WebPart_Sommaire_Liste_Lien_li’></li> »);
        jQuery(contenuLi).appendTo(elSommaireListe);
        var contenuLinkLi = jQuery(« <a class=’WebPart_Sommaire_Liste_Lien_a’ href=’#sommaireIndex » + (i – 1).toString() + « ‘> » + divh1ArrayIdea.innerHTML + « </a> »);
        jQuery(contenuLinkLi).appendTo(contenuLi);
    }
});

Avec un peu de CSS et des images cela devrait rendre quelque chose de pas mal du tout !

SharePoint 2010 : Gérer la planification des pages de publication

Un petit bout de code pour gérer planifier des pages de publication :

SPList list=web.Lists[« Pages »];
SPListItem item = list.GetItemById(…); // Récupération de l’élément à planifier

if (ScheduledItem.IsScheduledItem(item))
{
   ScheduledItem scheduledItem = ScheduledItem.GetScheduledItem(listItem);
   DateTime startDate = DateTimeNow;
   DateTime endDate = new DateTime(2013, 12, 31, 23, 59, 00);
   scheduledItem.StartDate = startDate;
   scheduledItem.EndDate = endDate;
   scheduledItem.ListItem.Update();
   scheduledItem.Schedule();
}

Publishing SharePoint 2010 : ajouter ses propres styles dans l’éditeur de contenu

Lorsque vous déployez un site de publication, le processus normal de personnalisation passe par la page maitre (souvent plusieurs) et la création de gabarits de pages adaptés. Dans la majorité des cas, il est également indispensable de modifier les styles CSS des styles de l’éditeur de contenu pour refléter le style global du site.

Il se peut également que vos utilisateurs aient également besoin de style spécifique à leurs besoins, par exemple un style de paragraphe particulier pour mettre en valeur son contenu :

image_180315B4

Pour ajouter vos propres styles dans le ruban d’édition, l’opération est relativement simple, alors pourquoi s’en passer !

1. Créer son style CSS applicable au contenu avec un nom dont le préfixe est ‘ms-rteStyle-‘ par exemple ms-rteStyle-ZoneImportant

Par exemple :

p.ms-rteStyle-BoiteImportant
{
padding:10px !important;
border-color: #FFC829;
    background-color: #FFF2CC;

    border-width: 5px;
    border-style: solid;
    border-radius: 20px 0px 20px 0px;
    line-height:2;
    width:200px;
    height:200px;
}

2. Ajouter l’attribut –ms-name qui spécifie du nom du style tel qu’il apparaitra dans le ruban d’édition

Par exemple :

p.ms-rteStyle-BoiteImportant
{
-ms-name: »Zone Important »;
padding:10px !important;

}

3. Inclure la référence – si ce n’est pas déjà fait – dans la page maitre

Par exemple (ici avec un déploiement de votre fichier CSS contenant votre définition dans la bibliothèques de style) :

<link rel= »stylesheet » type= »text/css » href= »http://blogs.developpeur.org/Style%20Library/StylesEdition.css » />

Remarque : c’est la dernière définition CSS appliquée au style qui donnera le nom de ce dernier. Si un autre CSS s’applique après celui que vous avez créé et dans lequel vous avez surchargé la définition (par exemple pour avoir une déclinaison de couleur différente spécifique en fonction d’un site), il vous faudra remettre le nom du style (via l’attribut -ms-name: »Titre mon style ») sinon votre thème aura un titre vide !

Et le tour est joué !

Le guide de déploiement de SharePoint 2013 RTM est dispo

En attendant la version RTM, voici le guide de déploiement de la version Preview de SharePoint 2013. Ce guide sera très sensiblement identique pour la version finale donc à prendre d’ores et déjà comme référence :

http://www.microsoft.com/en-us/download/confirmation.aspx?id=30384

Un programme très complet (de 1156 pages !) :

– Préparation des environnements (matériel et logiciel : Windows, SQL Server, etc des comptes Windows, )
– Installation des environnements pour différentes configurations : standalone, simple serveur SP + SQL, ferme de plusieurs serveurs, etc et installation des packs de langue.
– Installation dans un environnement virtualisé avec Hyper-V
– Configuration des services (en détail !)
– Configuration de la ferme
– Gestion des apps
– Bonnes pratiques
– Etc

Excellente (soirées de) lecture

Bug javascript ‘g_ExpGroupXSLTQueue’ du Service Pack 1 de la version Française SharePoint Server 2010

Update : le hotfix est disponible à cette adresse :http://support.microsoft.com/kb/2553117/en-us

Tout ceux qui ont déployé le SP1 en langue Française (que ce soit pour une migration ou une fresh install) ont peut-être constaté certains problèmes :

– Les regroupements des vues ne fonctionnent plus (la première fois, tout fonctionne, après rafraichissement de la page, les éléments des groupes n’apparaissent plus et il n’est plus possible d’ouvrir/fermer les groupes)

– Problème de lenteur à l’ouverture de la fenêtre ‘Télécharger un document’

– Problème de lenteur général dans l’affichage des listes, voire un freeze/plantage d’IE

Pour les utilisateurs d’Internet Exporer (Firefox ne remontant jamais les problèmes de javascript par défaut), vous devriez avoir un message similaire à“g_ExpGroupXSLTQueue’ is undefined”.

Le problème remonté depuis quelques semaines à Microsoft n’a toujours pas, semble t-il, trouvé de correctif officiel (dès que le correctif est publié, je mettrai à jour ce post). Le problème se situe dans les script core.js SP1 des langues non anglaises (Français, Italien, etc) et notamment dans les versions de code (14.0.4762 => 14.0.6009 ; merci David pour cette précision).

En effet, ce bug n’apparait pas dans la version anglaise des scripts. Par conséquent, afin de contourner ces soucis assez critique pour les utilisateurs, voici une méthode :

  1. Installer le pack de langue anglais EN
  2. Extrayez les fichiers init.js et init.debug.js contenus dans le répertoire “14\TEMPLATE\LAYOUTS\1033” et coller les (après avoir sauvergarder les versions localisées que vous allez remplacer) dans le répertoire “14\TEMPLATE\LAYOUTS\<LanguageID” soit pour les Français “14\TEMPLATE\LAYOUTS\1036”.

Après application de ces scripts :

  • Le problème disparait …
  • … mais les libellés des actions des menus contextuels se mettront en anglais. Charge à vous de traduire les quelques libellés qui posent problème avec un bon éditeur de texte ou de JS.

Voilà une méthode de contournement pas trop complexe à mettre en oeuvre, en attendant le correctif officiel …

SharePoint 2013 @Noumea : kickoff !

Le kickoff de SharePoiont 2013, Office 2013 … (la liste est longue pour cette déferlante MS) à l’autre bout du monde, à Nouméa, s’est déroulé le 07 Novembre 2013. Au programme du Windows 8 (avec un clin d’oeil particulier au nouvel Hyper V3 maintenant inclut dans Windows 8 !) pour le côté ergonomie et expérience utilisateur :

IMG_7125_thumb_237670E0

Suivi de Office 2013 et SharePoint 2013 (avec une machine qui ramait malgré 6 Go RAM, un SSD et 6 coeurs de core i7 !) :

IMG_7133_thumb_7E38DAD9

Bref SharePoint 2013 ça se passe ici aussi !

Les photos du MS Day Nouméa

SharePoint a bien atteint les coins les plus reculés du monde et notamment Nouméa avec cette édition qui a permis aux partenaires de s’exprimer sur leur sujet de prédilection : Lync, SharePoint, Project, Cloud, etc.

Tout ça accompagner par une XBOX 360 et la bonne humeur locale des invités.

Préparation de la salle

Une salle dans un hotel 4* fraichement ouvert, des affiches Microsoft partout, des petits-fours et des rafraichissement, nous voici bien dans un évènement Microsoft :

IMG_5933-Medium_342BD13C

IMG_5935-Medium_thumb_17AE1DB5

Je vous l’accorde nous ne sommes pas dans un TechDays parisien avec une salle de 200 personnes ou plus en délire. D’un certain point de vue, tant mieux, cela ne fait que renforcer la proximité avec son audience et rendre les sessions plus interactives.

SharePoint Server

Le produit phare du moment de Microsoft a eu le droit à deux sessions (niveau 100/200) :

  • Découverte des 10 fonctionnalités : GED, ECM, Recherche, etc  – Kelios Solutions
  • Les 10 points clé de la réussite d’un projet SharePoint 2010 – Kelios Solutions

P1070019_thumb_1BC7CFAD

(merci à Paul-Jorge pour les photos, il faudra investir dans un vrai appareil et plus dans une XBOX 360 pour l’année prochaine)

(pour ceux qui l’aurait reconnu, j’ai eu l’occasion de présenter les sessions avec un super polo MVP !)

A l’année prochaine (ou avant … Clignement d'œil ) !

Utilisation du service WorkflowQueuingService

L’exécution d’un workflow est souvent dépendante des entrées que les hommes (via une console ou une application Windows Form) ou les programmes (ASP.NET, Web Service, Mainframe, etc) feront dans votre workflow, même après plusieurs jours d’attente. Une activité qui attend des entrées d’une entité extérieure se doit de faire deux choses :

  • Notifier qu’elle attend des données (se mettre ‘au repos’)
  • Etre notifié quand les données seront mise à disposition.

Et cela, même si l’instance de son workflow est persistée (dans un fichier texte ou une base de données par exemple). Lorsque les données arrivent, le moteur d’exécution (le runtime) doit pouvoir recharger en mémoire l’instance et être capable de continuer l’exécution du workflow au bon endroit.

Comment faire en sorte de pouvoir spécifier au workflow qu’il est en attente d’une donnée et de traiter la donnée une fois celle-ci mise à la disposition de l’instance du workflow ? Comment assurer une communication ‘propre’ à l’intérieur des couches logicielles et de votre architecture ? Il y a plusieurs méthodes, plus ou moins robuste pour le faire,  une bonne méthode est néanmoins d’utiliser ce qui fait le fondement de WF : les files de Workflow !

De cette façon, à chaque fois que vous avez besoin de pousser une entrée vers le workflow, telle qu’une chaine de cractères ou tout autre objet (dans notre exemple, une transaction financière), vous ajoutez un élément à l’une ou les piles de l’instance du workflow. La pile se chargera à son tour d’appeler les méthodes abonnées aux évènements pour traiter ces entrées comme stimuli de l’activité. Ce principe nous permet d’utiliser le service WorkflowQueuingService qui s’exécute dans le moteur d’exécution de WF et qui va vous permettre d’ajouter et de nommer des files dans une instance de workflow.

L’autre avantage de cette méthode est le fait de pouvoir ainsi marquer des points d’exécution et des points de persistence dans votre workflow sans que vous ne soyez obligé d’être dans la portée d’une transaction.

Imaginons que nous voulons traiter un workflow simple qui attend le traitement d’une transaction financière par un serveur backend.

Voici la structure d’une transaction attendue comme entrée par le workflow :

public struct Transaction {

public double Amount;

public DateTime Date;

public Guid ID;

public Transaction(Guid id, double amount, DateTime date) {

this.ID = id; this.Amount = amount; this.Date = date;

}

public override string ToString() {

return string.Format(« ID: {0} – Amount: {1} – Date: {2} », ID, Amount, Date);

}

}

Commençons par implémenter une activité possédant une propriété contenant notre transaction  :

public partial class WaitProcessedTransaction : System.Workflow.ComponentModel.Activity

{

private Transaction mTransaction;

public Transaction Transaction

{

get { return mTransaction; }

set { mTransaction = value; }

}

}

Voici les méthodes de création (méthode d’initialisation de l’activité) et de destruction (méthode de ‘désinitialisation’, comprendre que cette méthode est appelée de façon synchrone lorsque de la transition de l’état de l’activité de Executing vers Closed) de la file de Workflow :

protected override void Initialize(IServiceProvider provider) {

WorkflowQueuingService qService =

(WorkflowQueuingService)provider.GetService(typeof(WorkflowQueuingService));

if (!qService.Exists(this.Name))

qService.CreateWorkflowQueue(this.Name, true);

}

protected override void Uninitialize(IServiceProvider provider) {

WorkflowQueuingService qService = (WorkflowQueuingService)provider.GetService(typeof(WorkflowQueuingService));

if (qService.Exists(this.Name))

qService.DeleteWorkflowQueue(this.Name);

}

}

On peut constater que les files sont nommées, ici par le nom de l’activité.

Nous allons redéfinir la méthode d’exécution de l’activité par la méthode suivante :

protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext) {

WorkflowQueuingService qService = executionContext.GetService<WorkflowQueuingService>();

WorkflowQueue queue = qService.GetWorkflowQueue(this.Name);

if (queue.Count > 0){

this.mTransaction = (Transaction)queue.Dequeue();

return ActivityExecutionStatus.Closed;

}

queue.QueueItemAvailable += this.ContinueAt;

return ActivityExecutionStatus.Executing;

}

void ContinueAt(object sender, QueueEventArgs e){

ActivityExecutionContext context = (ActivityExecutionContext)sender;

WorkflowQueuingService qService = context.GetService<WorkflowQueuingService>();

WorkflowQueue queue = qService.GetWorkflowQueue(this.Name);

this.mTransaction = (Transaction)queue.Dequeue();

context.CloseActivity();

}

Dans la méthode d’exécution, nous récupérons le service de file puis nous extrayons l’entrée qui pourrait potentiellement se trouver dans la file. Si la donnée est récupérée de la file, nous spécifions la clôture de l’activité, sinon nous abonnons une méthode à l’évènement qui sera appelée dés qu’une entrée dans la file sera disponible, puis nous laissons le statut de l’activité comme ‘en cours d’exécution’.

Maintenant c’est au tour de l’application de jouer son rôle en fournissant l’entrée dans la file nommée de l’instance du workflow :

// Création d’une instance de workflow et démarrage

WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(WorkflowDemo.WorkflowQueueTest));

instance.Start();

// On crée une transaction financière et la met dans la file nommée ‘WaitTransactionProcessed’ (qui est le nom de l’activité ciblée) ce qui aura pour effet de réveiller le workflow et d’exécuter la méthode abonnée à l’évènement ‘QueueItemAvailable’ de la file (méthode ‘ContinueAt’)

Transaction t = new Transaction(Guid.NewGuid(), 1000.0, DateTime.Now);

instance.EnqueueItem(« AttendreTraitementTransaction », t, null, null);

En utilisant un objet d’une instance de workflow, et cela n’importe où dans l’application hôte, vous allez pouvoir marquer des points d’exécution dans votre workflow grâce aux files et communiquer une ou plusieurs entrées à votre activité pour pouvoir continuer son exécution. Cela est vrai même si celui-ci a été persisté pendant son état de repos.

Dans notre cas, nous parlons d’un workflow qui peut attendre un stimulus d’une entité extérieure sur des périodes plutôt longues, il faut donc s’attendre à se que le workflow soit décharger de la mémoire pour être persisté à un endroit physique. Dans notre cas nous allons le faire persister dans une base SQL Server 2005 (classique en quelque sorte) en utilisant le service SqlWorkflowPersistenceService (voici un lien pour mettre en place tout ce qu’il faut : http://msdn2.microsoft.com/en-us/library/aa349366.aspx).

clip_image002

Note : voici également un excellent schéma des états d’un workflow : http://msdn2.microsoft.com/en-us/library/aa663362.hostingwwf03l%28en-us,msdn.10%29.jpg

Voici les quelques lignes qu’il faut rajouter pour configurer le runtime :

string stringConnection = @ »Initial Catalog=WF_Persistence;Data Source=localhost\SQLEXPRESS;User Id=x;Password=x »;

// Création d’une instance du service de persistence Sql Server avec un argument à true pour spécifier de persister une instance de workflow dés que son état passe au repos

SqlWorkflowPersistenceService persistenceService = new SqlWorkflowPersistenceService(

stringConnection, true, TimeSpan.MaxValue, new TimeSpan(0, 1, 0));

workflowRuntime.AddService(persistenceService);

Pour mieux comprendre ce qui se passe on rajoute des handle à différents évènements qui afficheront les changements d’état :

workflowRuntime.WorkflowIdled += new EventHandler<WorkflowEventArgs>(workflowRuntime_WorkflowIdled);

workflowRuntime.WorkflowPersisted += new EventHandler<WorkflowEventArgs>(workflowRuntime_WorkflowPersisted);

workflowRuntime.WorkflowLoaded += new EventHandler<WorkflowEventArgs>(workflowRuntime_WorkflowLoaded);

workflowRuntime.WorkflowCompleted += new EventHandler<WorkflowCompletedEventArgs>(workflowRuntime_WorkflowCompleted);

void workflowRuntime_WorkflowCompleted(object sender, WorkflowCompletedEventArgs e) { Console.WriteLine(« Workflow completed ! »); }

void workflowRuntime_WorkflowLoaded(object sender, WorkflowEventArgs e){ Console.WriteLine(« Workflow loaded ! »); }

void workflowRuntime_WorkflowPersisted(object sender, WorkflowEventArgs e) { Console.WriteLine(« Workflow persisted ! »); }

void workflowRuntime_WorkflowIdled(object sender, WorkflowEventArgs e) { Console.WriteLine(« Workflow idle ! »); }

Imaginons que nous avons un workflow séquentiel contenant notre activité qui attend le traitement d’une transaction lancée en amont d’un workflow, et d’une activité qui affiche un message de confirmation du traitement :

clip_image004

Nous pourrons observer le résultat suivant :

clip_image006

Voici un cas simple d’utilisation du service WorkflowQueuingService. Ce service est à la base de nombreuses activités et reste un moyen élégant et efficace de placer des points d’attente nommés – par le nom de la file – où vos activités peuvent recevoir des données, c’est à dire des endroits logiques dans votre workflow dont l’exécution peut être reprise quand un stimuli spécifique (une données, un évènement, …) arrive, tout en étant capable d’être persisté pendant le temps d’attente.

Le lien MSDN vers le WorkflowQueuingService qui devrait être à la base de beaucoup de vos activités communicant avec l’extérieur (permet de remplacer très élégamment l’activité HandleExternalEvent : http://msdn2.microsoft.com/en-us/library/system.workflow.runtime.workflowqueuingservice.aspx