Interroger une liste SharePoint depuis le webservice exposé qui va bien n’est pas très difficile mais c’est un peu casse pied de manipuler le flux XML retourné et de jouer avec le langage de requêtes de SharePoint appelé CAML. Voyons donc comment utiliser les dernières technologies disponibles avec le .NET Framework 3.5 telle que LINQ pour rendre meilleure notre vie de développeur.

Partons de la liste d’exemple suivante :

3685013544

Notre but ici sera de se créer une petite librairie permettant d’interroger le WebService de Sharepoint pour manipuler cette liste et ses éléments via un mapping objet et grâce l’utilisation de LINQ.

La méthode classique

Ajoutons une référence au service Web qui va bien en faisant « Add Service Reference » sur le projet, puis « Advanced » puis « Add Web Reference.. ». Dans l’URL, entrez n’importe quelle adresse pointant vers un serveur SharePoint suivit ensuite de « /_vti_bin/Lists.asmx ». Dans mon cas, je nommerais la référence « MOSSListWS ».

3684201643

Cela va nous construire le proxy client que l’on pourra bien évidement utiliser vers d’autres serveurs SharePoint.

Si l’on appelle la méthode GetListItems() sur la liste précédente via le code suivant :

// On instancie le proxy du webservice SPS

MOSSListWS.Lists list_svc = new MOSSListWS.Lists();

// On utilise l'identité actuelle comme Credentials pour

// appeler le Webservice

list_svc.Credentials = CredentialCache.DefaultCredentials;

 

// On indique au proxy l'URL cible

list_svc.Url = _sharePointURL;

 

// On récupère tous les enregistrements

XmlNode itemCollection = list_svc.GetListItems(_nomListe, string.Empty, null, null, "0", null, "");

Voici le type de XML qui est retourné par la méthode dans itemCollection :

<rs:data ItemCount="7" xmlns:rs="urn:schemas-microsoft-com:rowset">

  <z:row ows_Attachments="0" ows_LinkTitle="Halo 3"

         ows_Plateforme="Xbox 360"

         ows_Description="Le meilleur jeu de FPS sur console"

         ows_Prix="60.0000000000000" ows_MetaInfo="1;#"

         ows__ModerationStatus="0"

         ows__Level="1"

         ows_Title="Halo 3"

         ows_ID="1"

         ows_owshiddenversion="1"

         ows_UniqueId="1;#{03EF1580-F6DE-4BAA-9D80-7665207789CB}"

         ows_FSObjType="1;#0"

         ows_Created_x0020_Date="1;#2009-06-23 16:02:06"

         ows_Created="2009-06-23 16:02:06"

         ows_FileLeafRef="1;#1_.000"

         ows_FileRef="1;#sites/davrous/Lists/Liste Produits/1_.000"

         xmlns:z="#RowsetSchema" />

  <z:row ows_Attachments="0" ows_LinkTitle="Visual C# Express" ows_Plateforme="Windows" ows_Description="Le meilleur éditeur de code gratuit du marché :)" ows_Prix="0" ows_MetaInfo="2;#" ows__ModerationStatus="0" ows__Level="1" ows_Title="Visual C# Express" ows_ID="2" ows_owshiddenversion="1" ows_UniqueId="2;#{2031D3B5-F3B8-4597-9567-137ACCD2C1DB}" ows_FSObjType="2;#0" ows_Created_x0020_Date="2;#2009-06-23 16:02:49" ows_Created="2009-06-23 16:02:49" ows_FileLeafRef="2;#2_.000" ows_FileRef="2;#sites/davrous/Lists/Liste Produits/2_.000" xmlns:z="#RowsetSchema" />

</rs:data>

Il faudrait donc parser ce morceau de XML soit à la main soit via du XPath pour retrouver les informations qui nous intéressent. Je ne sais pas vous, mais moi, le XPath je ne suis pas super fan.

Création d’un mapping objet

Sur la copie d’écran de notre liste « Liste Produits », 4 colonnes sont utilisées: Nom du produit, Plateforme, Description et Prix.

On va donc partir sur cette définition objet pour faire le mapping avec la liste :

public class Produit

{

    public string Nom { get; set; }

    public string Description { get; set; }

    public string Plateforme { get; set; }

    public double Prix { get; set; }

}

On va ensuite créer une classe de base nommée BaseSPSList qui fonctionnera avec n’importe quelle type de liste SharePoint, quelques soient les colonnes qui ont été définies mais qui du coup offrira très peu de services : simplement récupérer les valeurs de la 1er colonne. Puis ensuite une classe héritant de BaseSPSList nommée ProductsSPSList qui sera spécialisée et dédiée à la liste « Liste Produits » telle que définie par la 1ère copie d’écran.

Dans cette classe spécialisée, nous ferons donc appel au type Produit et nous aurons accès à des méthodes pour créer un nouvel enregistrement dans la liste, mettre à jour un enregistrement et supprimer un enregistrement.

Pour éviter un aller/retour systématique vers le serveur SharePoint, un petit mécanisme de cache sera mis en place. Par ailleurs, dans cet exemple de code, j’ai fait le choix de retourner l’ensemble des enregistrements de la liste. Cela n’est pas forcément optimal. On peut éventuellement demander à SharePoint, via l’appel à la méthode GetListItems(), de ne retourner qu’un sous-ensemble grâce à l’utilisation de requêtes appelées CAML. Autre approximation dans cet exemple, on va considérer que le nom du produit nous servira comme clé pour les mises à jour et suppression depuis notre couche objet.

Bref, certaines choses sont mal et devront être revues si vous avez l’idée saugrenue de reprendre ce code en production.

Voici le code que je vous propose, rendez-vous après celui-ci pour quelques commentaires additionnels. A tout de suite donc !

// Classe de base pour les opérations communes à toutes les listes

public class BaseSPSList

{

    // URL du serveur SharePoint cible

    protected string _sharePointURL;

    // nom de la liste

    protected string _nomListe;

    // on se sert d'un cache mémoire pour éviter

    // trop d'aller/retour serveur

    protected IEnumerable<XElement> cachedSPSXElements = null;

    // Du coup, il faut pouvoir regénérer le cache si besoin

    public bool IsCacheDirty { get; set; }

 

    // Le constructeur a besoin de l'URL du site SharePoint ainsi

    // que la liste visée contenu dans ce même site

    public BaseSPSList(string SharePointURL, string nomListe)

    {

        this._sharePointURL = SharePointURL + "/_vti_bin/Lists.asmx";

        this._nomListe = nomListe;

        this.IsCacheDirty = true;

    }

 

    // Exemple de méthode qui fonctionne quelque soit la liste

    // fournie vu que ows_LinkTitle est systématiquement utilisé

    // par SharePoint 2007 comme nom de 1ère colonne

    public IEnumerable GetDistinctFirstColumnValues()

    {

        if (IsCacheDirty || cachedSPSXElements == null) BuildFirstQuery();

 

        var query = from x in cachedSPSXElements

                    where x.Attribute("ows_LinkTitle") != null

                    select new

                    {

                        Title = x.Attribute("ows_LinkTitle").Value

                    };

 

        var resultat = (from f in query

                        select f.Title).Distinct();

 

        return resultat;

    }

 

    // On appel cette méthode que lors de la 1ere action effectuée

    protected void BuildFirstQuery()

    {

        // On instancie le proxy du webservice SPS

        MOSSListWS.Lists list_svc = new MOSSListWS.Lists();

        // On utilise l'identité actuelle comme Credentials pour

        // appeler le Webservice

        list_svc.Credentials = CredentialCache.DefaultCredentials;

 

        // On indique au proxy l'URL cible

        list_svc.Url = _sharePointURL;

 

        // On récupère tous les enregistrements

 // TO DO: ajouter la possibilité de spécifier un filtre

        XmlNode itemCollection = list_svc.GetListItems(_nomListe, string.Empty, null, null, "0", null, "");

 

        // On parse le flux XML retourné par le webservice

        // pour faire appel ensuite à LINQ2XML

        XDocument cachedXMLDocument;

        cachedXMLDocument = XDocument.Parse(itemCollection.OuterXml.ToString());

        // Namespace spécifique au XML retourné par SharePoint

        XNamespace ns = "#RowsetSchema";

        cachedSPSXElements = cachedXMLDocument.Descendants(ns + "row");

        IsCacheDirty = false;

    }

}

 

// Classe spécialisée sur notre définition de liste

// comme "Liste Produits"

public class ProductsSPSList : BaseSPSList

{

    // Constante spécifique à SharePoint pour

    // son nommage interne via son schéma de liste

    const string SPS_INT_NOMPRODUIT = "ows_LinkTitle";

    const string SPS_INT_PLATEFORMES = "ows_Plateforme";

    const string SPS_INT_DESCRIPTION = "ows_Description";

    const string SPS_INT_PRIX = "ows_Prix";

    const string SPS_INT_SharePointID = "ows_ID";

 

    const string SPS_NOMPRODUIT = "Title";

    const string SPS_PLATEFORMES = "Plateforme";

    const string SPS_DESCRIPTION = "Description";

    const string SPS_PRIX = "Prix";

 

    public ProductsSPSList(string SharePointURL, string ListName)

        : base(SharePointURL, ListName)

    {

    }

 

    // Méthode retournant l'ensemble des produits de la liste

    public IEnumerable<Produit> GetProducts()

    {

        System.Globalization.CultureInfo ci;

        ci = System.Globalization.CultureInfo.CreateSpecificCulture("en-us");

 

        // Si c'est le 1er appel ou si le cache n'est plus considéré

        // comme à jour, on rappatrie les enregsitrements depuis SharePoint

        // sinon on utilise le cache mémoire pour éviter un aller/retour

        if (IsCacheDirty || cachedSPSXElements == null) BuildFirstQuery();

 

        // Requete LINQ permettant de faire le mapping

        // entre le flux XML brut retourné par SharePoint

        // et notre modèle de données Produit

        var produits = from x in cachedSPSXElements

                       where x.Attribute("ows_LinkTitle") != null

                       select new Produit

                       {

                           Nom = x.Attribute(SPS_INT_NOMPRODUIT).Value,

                           // permet de gérer les cas où la colonne n'a pas de valeur

                           // renseignée pour ce produit

                           Plateforme = x.Attribute(SPS_INT_PLATEFORMES) != null ? x.Attribute(SPS_INT_PLATEFORMES).Value : string.Empty,

                           Description = x.Attribute(SPS_INT_DESCRIPTION) != null ? x.Attribute(SPS_INT_DESCRIPTION).Value : string.Empty,

                           Prix = x.Attribute(SPS_INT_PRIX) != null ? double.Parse(x.Attribute(SPS_INT_PRIX).Value, ci) : -1,

                           SharePointID = x.Attribute(SPS_INT_SharePointID) != null ? x.Attribute(SPS_INT_SharePointID).Value : string.Empty,

                       };

 

        return produits;

    }

 

    // Retourne le produit correspondant au nom passé en argument

    public IEnumerable<Produit> GetProductByName(string nomProduit)

    {

        IEnumerable<Produit> produits = GetProducts();

 

        var filtreProduits = from produit in produits

                             where produit.Nom == nomProduit

                             select produit;

 

        return filtreProduits;

    }

 

    // Méthode pour mettre à jour un produit coté liste SharePoint

    // Si le produit existe déjà, il est mise à jour

    // Sinon, il est tout simplement créé

    public bool UpdateProduct(Produit majProduit)

    {

        // On regarde si le produit existe déjà ou pas dans le cache

        IEnumerable<Produit> produitExistant = GetProductByName(majProduit.Nom);

        try

        {

            // Il n'a pas été trouvé en mémoire

            // Il faut donc le créer coté SharePoint

            if (produitExistant.Count().Equals(0))

            {

                // Il faut indiquer d'après cette opération

                // le cache ne refletera plus la liste SharePoint

                IsCacheDirty = true;

                return AddNewProduct(majProduit);

 

            }

            else

            {

                majProduit.SharePointID = produitExistant.First().SharePointID;

                return UpdateExistingProduct(majProduit);

            }

        }

        catch (Exception)

        {

            return false;

        }

    }

 

    public bool DeleteProduct(Produit supProduit)

    {

        IEnumerable<Produit> produitExistant = GetProductByName(supProduit.Nom);

        try

        {

            // Il n'a pas été trouvé en mémoire

            // Nous avons donc rien à faire

            if (produitExistant.Count().Equals(0))

            {

                return true;

 

            }

            else

            {

                // Le cache ne sera plus à jour après cette opération

                IsCacheDirty = true;

                string SharePointID = produitExistant.First().SharePointID;

                return DeleteExistingProduct(SharePointID);

            }

        }

        catch (Exception)

        {

            return false;

        }

    }

 

    // Méthode pour ajouter un nouveau produit

    private bool AddNewProduct(Produit nouveauProduit)

    {

        try

        {

            MOSSListWS.Lists list_svc = new MOSSListWS.Lists();

            list_svc.Credentials = CredentialCache.DefaultCredentials;

 

            list_svc.Url = _sharePointURL;

 

            XmlDocument doc = new XmlDocument();

            XmlElement batch_element = doc.CreateElement("Batch");

 

            batch_element.SetAttribute("ListVersion", "1");

 

            string item = "<Method ID=\"1\" Cmd=\"New\">" +

                "<Field Name=\"ID\">New</Field>"

                + "<Field Name=\"" + SPS_NOMPRODUIT + "\">" + nouveauProduit.Nom + "</Field>"

                + "<Field Name=\"" + SPS_PLATEFORMES + "\">" + nouveauProduit.Plateforme + "</Field>"

                + "<Field Name=\"" + SPS_DESCRIPTION + "\">" + nouveauProduit.Description + "</Field>"

                + "<Field Name=\"" + SPS_PRIX + "\">" + nouveauProduit.Prix + "</Field>"

                + "</Method>";

 

            batch_element.InnerXml = item;

 

            // Aucune exception n'est levée en cas de non ajout

            // Il faut donc vérifier l'errorcode dans retour XML

            XmlNode resultat = list_svc.UpdateListItems(_nomListe, batch_element);

 

            return resultat.InnerText.Equals("0x00000000");

        }

        catch (Exception ex)

        {

            string err = ex.Message;

            return false;

        }

    }

 

    // Methode pour mettre à jour un enregistrement coté SharePoint

    private bool UpdateExistingProduct(Produit majProduit)

    {

        try

        {

            MOSSListWS.Lists list_svc = new MOSSListWS.Lists();

            list_svc.Credentials = CredentialCache.DefaultCredentials;

 

            list_svc.Url = _sharePointURL;

 

            XmlDocument doc = new XmlDocument();

            XmlElement batch_element = doc.CreateElement("Batch");

 

            batch_element.SetAttribute("ListVersion", "1");

 

            string item = "<Method ID=\"1\" Cmd=\"Update\">" +

                "<Field Name=\"ID\">" + majProduit.SharePointID + "</Field>"

                 + "<Field Name=\"" + SPS_NOMPRODUIT + "\">" + majProduit.Nom + "</Field>"

                + "<Field Name=\"" + SPS_PLATEFORMES + "\">" + majProduit.Plateforme + "</Field>"

                + "<Field Name=\"" + SPS_DESCRIPTION + "\">" + majProduit.Description + "</Field>"

                + "<Field Name=\"" + SPS_PRIX + "\">" + majProduit.Prix + "</Field>"

                + "</Method>";

 

            batch_element.InnerXml = item;

            XmlNode resultat = list_svc.UpdateListItems(_nomListe, batch_element);

 

            return resultat.InnerText.Equals("0x00000000");

        }

        catch (Exception ex)

        {

            string err = ex.Message;

            return false;

        }

    }

 

    // Méthode pour supprimer un enregistrement coté SharePoint

    private bool DeleteExistingProduct(string SharePointID)

    {

        try

        {

            MOSSListWS.Lists list_svc = new MOSSListWS.Lists();

            list_svc.Credentials = CredentialCache.DefaultCredentials;

 

            // The URL property for WebService retrieve

            list_svc.Url = _sharePointURL;

 

            XmlDocument doc = new XmlDocument();

            XmlElement batch_element = doc.CreateElement("Batch");

 

            batch_element.SetAttribute("ListVersion", "1");

 

            string item = "<Method ID=\"1\" Cmd=\"Delete\">" +

                "<Field Name=\"ID\">" + SharePointID + "</Field></Method>";

 

            batch_element.InnerXml = item;

            XmlNode resultat = list_svc.UpdateListItems(_nomListe, batch_element);

 

            return resultat.InnerText.Equals("0x00000000");

        }

        catch (Exception ex)

        {

            string err = ex.Message;

            return false;

        }

    }

 

    // Si vous avez besoin d'ajouter des dates de .NET vers une liste SharePoint

    // il faut la convertir au format ISO 8601

    private static String ConvertDateTimeToISO8601(DateTime dt)

    {

 

        return dt.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'");

    }

 

    private static DateTime ConvertISO8601ToDateTime(String iso)

    {

        return DateTime.ParseExact(iso, "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'", System.Globalization.CultureInfo.InvariantCulture);

    }

}

J’espère avoir suffisamment commenté le code pour qu’il soit compréhensible en 1ère lecture. La dernière partie est un cadeau bonus si comme moi vous galérez à un moment pour convertir un type DateTime de .NET défini dans votre mapping objet vers le format date de SharePoint. On ne l’utilise pas dans cet exemple mais cela peut toujours vous servir.

Si vous avez été attentif, vous avez probablement remarqué les constantes que j’ai surlignées en jaune. Comment ai-je deviné leurs valeurs ? En analysant tout bêtement le morceau de XML mentionné en début d’article. Une meilleure approche sera d’interroger le service Web de liste via la méthode GetList() pour récupérer le schéma XML défini par SharePoint pour ma liste d’exemple et générer le mapping objet de manière automatique en analysant le schéma plutôt que de le coder en dur comme je l’ai fait.

Toujours est-il, une fois que vous avez compris le concept, voici ensuite le type de client pouvant faire appel au code précédent s’il est mis dans une librairie DLL :

#region Section Lecture de la liste

// on instancie notre super Helper

ProductsSPSList monHelper = new ProductsSPSList("http://myemea/sites/davrous", "Liste Produits");

 

// on souhaite récupérer l'ensemble des produits

// inscrits dans la liste SharePoint "Liste Produits"

var tousLesProduits = monHelper.GetProducts();

 

// On peut alors simplement parcourir les produits

// un à un

foreach (Produit p in tousLesProduits)

{

    Console.WriteLine(p.Nom + " - Plateforme : " + p.Plateforme);

}

 

Console.WriteLine("-------------------");

 

// Encore mieux, comme nous retournous un

// IEnumerable depuis notre librairie, on peut

// filtrer le retour avec une requête LINQ!

var produitsXbox360 = from produits in tousLesProduits

                      where produits.Plateforme.Equals("Xbox 360")

                      select produits;

 

// On peut à nouveau parcourir la projection

foreach (Produit p in produitsXbox360)

{

    Console.WriteLine(p.Nom);

}

#endregion

 

#region Section Mise à jour de la liste

// Mettons à jour d'un produit existant en le recherchant

// d'abord par son nom

Produit Natal = monHelper.GetProductByName("Natal").First();

// On change le prix

// Note : ce prix est le fruit de mon imagination, n'allez

// surtout pas en déduire quoique ce soit

Natal.Prix = 50;

 

// On lance l'opération de mise à jour coté serveur SharePoint

if (monHelper.UpdateProduct(Natal))

    Console.WriteLine("Mise à jour de {0} effectuée avec succès.", Natal.Nom);

else

    Console.WriteLine("Erreur lors de la mise à jour de {0}.", Natal.Nom);

 

// Testons maintenant la création d'un nouveau produit

Produit nouveauProduit = new Produit();

nouveauProduit.Nom = "Zune HD";

nouveauProduit.Plateforme = "Xbox 360";

nouveauProduit.Description = "un super baladeur qui envoit";

nouveauProduit.Prix = 35;

 

// Même méthode que précédemment sauf que cette fois-ci

// nous allons detécter l'absence du produit dans le cache

// donc on va lancer l'opération de création coté SharePoint

if (monHelper.UpdateProduct(nouveauProduit))

    Console.WriteLine("Création de {0} effectuée avec succès.", nouveauProduit.Nom);

else

    Console.WriteLine("Erreur lors de la création de {0}", nouveauProduit.Nom);

#endregion

 

#region Section Suppression de la liste

// Sitôt créé, sitôt détruit

// La vie d'un produit n'est pas toujours rose vous savez...

Produit Zune = monHelper.GetProductByName("Zune HD").First();

if (monHelper.DeleteProduct(Zune))

    Console.WriteLine("Suppression de {0} effectuée avec succès.", Zune.Nom);

else

    Console.WriteLine("Erreur lors de la suppression de {0}", Zune.Nom);

#endregion

 

Console.ReadLine();

Avouez que c’est nettement plus agréable à manipuler de cette façon ? En partant de la liste présentée en début d’article, voici le résultat de l’exécution de ce code client :

3685013748

Vous trouverez ci-dessous un fichier ZIP contenant ces exemples de code sous forme de 2 projets : 1 projet de type librairie donc contenant la logique du helper et 1 projet de type application console référençant la DLL puis faisant appel au helper comme ci-dessus.

Pour aller plus loin, on pourrait alors imaginer un générateur de code / création de mapping relationnel similaire à ce que nous avons aujourd’hui avec LINQ to SQL ou Entity Framework mais connecté à un type SharePoint. Il existe justement un projet de ce genre sur notre plateforme de partage de codes source ici : http://www.codeplex.com/LINQtoSharePoint . Malheureusement inachevé (le support des mises à jour n’existe pas par exemple), le code source du projet n’attend que vous pour continuer son bonhomme de chemin !

David