Accueil
Rechercher:
sur developpez.com sur les forums
Forums | Tutoriels | F.A.Q's | Participez | Hébergement | Contacts
Club Emploi Blogs   TV   Dév. Web PHP XML Python Autres 2D-3D-Jeux Sécurité Windows Linux PC Mac
Accueil Conception Java DotNET Visual Basic  C  C++ Delphi MS-Office SQL & SGBD Oracle  4D  Business Intelligence
FORUM OFFICE FAQs OFFICE TUTORIELS OFFICE LIVRES OFFICE SOURCES VBA ACCESS

Les colonnes personnelles (custom field) avec Sharepoint

Date de publication : 28/01/2008

Dans ce tutoriel, nous allons aborder le développement de colonnes personnelles, comprendre leur intérêt et analyser leurs diverses possibilités au travers d'un exemple concret.

               Version PDF

I. Pré-requis pour bien comprendre ce tutoriel
II. Introduction
III. Aperçu de notre projet
IV. Préambule avant de parler du code
V. Le code
IV-A. Le code de la colonne
IV-B. Le code gérant le contrôle
IV-C. Le code gérant notre propriété personnelle
VI. Le fichier FLDTYPES_xxx.xml décrivant le comportement de notre colonne
VI-A. La définition de la colonne proprement dite
VI-B. Le DisplayPattern
VI-C. Autres patterns
VI-D. La propriété
VI. Déploiement
VIII. Téléchargement


I. Pré-requis pour bien comprendre ce tutoriel

Pour bien comprendre ce tutoriel, vous devez idéalement avoir des connaissances dans les domaines suivants:

  • Connaître l'ASP.NET
  • Signer et déployer des assemblages en GAC (voir autres tutos Sharepoint si vous ne connaissez pas)
  • Connaître les bases de Sharepoint, à savoir les sites, les différents types de listes, les différents types de colonnes

II. Introduction

L'exemple téléchargeable développé pour ce tutoriel permet à l'utilisateur de créer un lien à partir d'un document stocké dans une bibliothèque du site courant ou de ses sous-sites voire de la collection toute entière lorsque le site courant est le top level site. Les quelques images ci-dessous montrent l'utilisation de cette colonne personnelle. Je me suis inspiré du type de contenu existant "Link to document" en l'améliorant un peu dans la mesure où l'utilisateur ne doit pas saisir l'URL du document manuellement.

Lors de l'ajout de la colonne à une bibliothèque ou à une liste, on peut spécifier si on veut limiter le document au site courant ou s'il se limite à la collection.

Figure 1

En édition de propriétés ou en ajout d'élément, on obtient ceci

Figure 2

En somme des listes liées qui nous permettent de sélectionner le document vers lequel on souhaite créer un lien

Figure 3

Figure 4

Enfin, lorsque l'on a sélectionné le document cible et enregistré nos changements, on se retrouve avec une vue similaire à ceci

Figure 5

Une image symbolise le lien vers notre document. C'est d'ailleurs l'image standard utilisée par le système pour le type de contenu Link To Document

Tout ceci est réalisé au travers d'une seule et même colonne personnelle. Vous l'aurez compris, l'utilité des colonnes personnelles est de permettre au développeur de proposer un nouveau type de colonne offrant des fonctionnalités non supportées par les types de colonnes standard. Voici une liste non exhaustive des possibilités offertes par les colonnes personnelles

  • Pointer vers n'importe quelle source de données (DB externe, site Sharepoint courant et autre site, ....)
  • Effectuer des validations de saisie particulières
  • Appliquer des validations entre les différentes colonnes, ex: on sélectionne un continent et on est obligé de sélectionner un pays appartenant à celui-ci
  • Appliquer un affichage particulier sur la colonne.
  • ....
Maintenant que vous avez une idée plus précise de l'exemple développé et de l'utilité des colonnes personnelles, nous allons voir comment ceci a été réalisé.


III. Aperçu de notre projet

Voici la vue de notre solution et le détail de chaque fichier important

Figure 6

  • Reférences: les références vers les DLL Sharepoint et Web
  • Dans le répertoire CONTROLTEMPLATES, nous retrouvons nos deux contrôles utilisateurs qui serviront à gérer la saisie de la valeur de notre colonne et la propriété (checkbox illustrée en figure 1) liée à celle-ci
  • Dans le répertoire XML, on retrouve le fichier contenant la description de notre colonne.
  • GAC est un répertoire servant au déploiement, il contiendra la DLL de notre colonne personnelle devant être déployée en GAC
  • DocumentLink.snk : fichier de signature de notre assemblage
  • DocumentLink.cs : classe principale de notre colonne
  • DocumentLinkFieldControl.cs : implémentation de notre contrôle et interaction avec le contrôle utilisateur DocumentLink.ascx
  • DocumentProperties.cs : gestion de notre propriété "Scope" et interaction avec le contrôle utilisateur DocumentLinkEditorControl.ascx
  • GenerateSolution.bat : fichier appelé par Visual Studio au post-build pour générer et déployer la solution.

IV. Préambule avant de parler du code

J'ai développé cet exemple à titre purement pédagogique. Il va de soi qu'il n'est sans doute pas utilisable tel quel dans un système de production. Par exemple, lister tous les sites d'une collection peut s'avérer très coûteux en terme de performance et en ressources mémoire et il est plus judicieux d'utiliser un contrôle treeview dont on chargera les différents niveaux au fur et à mesure.

En outre, la méthode qui liste tous les documents d'une bibliothèque ne fonctionnne que pour une bibliothèque n'ayant pas de répertoires.

La gestion d'erreur est volontairement très réduite afin de ne pas alourdir le code pour la lisibilité et faciliter la compréhension des colonnes personnelles. Il va de soi qu'il faut être plus rigoureux dans un système de production.

Enfin, cet exemple n'est également pas localisé, les libellés etc..sont codés en dur.


V. Le code


IV-A. Le code de la colonne


using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint;

namespace DocumentLink
{
    /// <summary>
    /// Exemple réalisé par Stéphane Eyskens pour Developpez.com    /// 
    /// </summary>
    public class DocumentLnk : SPField
    {
        //Permet de gérer la propriété
        private static Dictionary<int, bool> UpdateProp = new Dictionary<int, bool>();
        
        //Constructeur
        public DocumentLnk(SPFieldCollection fields, string fieldName)
            : base(fields, fieldName)
        {

            Init();

        }
        //Surcharge de constructeur
        public DocumentLnk(Microsoft.SharePoint.SPFieldCollection fields, string typeName, string displayName)
            : base(fields, typeName, displayName)
        {

            Init();

        }
        //Instanciation de l'objet qui gère la saisie des données de notre colonne
        public override Microsoft.SharePoint.WebControls.BaseFieldControl FieldRenderingControl
        {
            get
            {               
                
                Microsoft.SharePoint.WebControls.BaseFieldControl Ctrl = new DocumentLinkFieldControl();
                Ctrl.FieldName = InternalName;
                DocumentLinkFieldControl PropInstance = Ctrl as DocumentLinkFieldControl;
                PropInstance.LimitToCurrentSite = this.LimitToCurrentSite;
                return Ctrl;

            }
        }
        //On récupère la valeur de la propriété LimitToCurrentSite
        private void Init()
        {
            this.LimitToCurrentSite = (this.GetCustomProperty("LimitToCurrentSite") != null) 
                ? (bool)this.GetCustomProperty("LimitToCurrentSite") : false;            
            
        }


        // Notre Propriété
        private bool _LimitToCurrentSite;

        public bool LimitToCurrentSite
        {

            get
            {

                return UpdateProp.ContainsKey(ContextId) ? UpdateProp[ContextId] : _LimitToCurrentSite;                                

            }

            set
            {

                this._LimitToCurrentSite = value;

            }

        }
        //On stocke la valeur de notre propriété
        public override void Update()
        {

            this.SetCustomProperty("LimitToCurrentSite", LimitToCurrentSite);

            base.Update();
            if (UpdateProp.ContainsKey(ContextId))
                UpdateProp.Remove(ContextId);

        }
        //Lorsque la colonne est ajoutée
        public override void OnAdded(SPAddFieldOptions op)
        {
            base.OnAdded(op);
            Update();
        }
        //Stocke la valeur de notre propriété dans un dictionnaire
        public void UpdatePropMethod(bool value)
        {

            UpdateProp[ContextId] = value;

        }
        //On s'assure que le contextid est unique.
        public int ContextId
        {

            get
            {

                return SPContext.Current.GetHashCode();

            }

        }
	}
}
Les points importants à retenir sont:

  • On dérive de SPFieldText
  • Les quelques méthodes nous permettant de gérer la valeur de notre propriété (plus de détail dans les sections suivantes)
  • L'implémentation de FieldRenderingControl qui nous permet de définir le comportement de notre colonne en mode édition/visualisation
Notez que pour ce contrôle, j'aurais pu dériver de SPFieldUrl, cela m'eût simplifié la tâche mais je préfèrais dériver de SPFieldText pour pouvoir vous parler du DisplayPattern (voir plus bas). Avec un SPFieldUrl, je n'aurais pas dû créer de DisplayPattern particulier.

En fonction de ce que l'on souhaite faire, on peut hériter des types de champs suivants:

SPField
SPFieldText
SPFieldUrl
SPFieldLookup
SPFieldChoice
SPFieldMultiColumn
SPFieldBoolean
SPFieldCalculated
SPFieldDateTime
etc...
Très souvent SPFieldText fait l'affaire mais si vous avez besoin de stocker plusieurs valeurs, vous vous orienterez vers SPFieldMultiColumn par exemple. Il ne faut pas non plus hésiter à étudier ce que Sharepoint fait en standard et voir les types qu'il utilise et comment il les utilise grâce à Reflector qui permet de désassembler les DLL dotnet donc celles de Sharepoint également.


IV-B. Le code gérant le contrôle


using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint.WebControls;
using Microsoft.SharePoint;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.Configuration;
using System.Data;



namespace DocumentLink
{
    public class DocumentLinkFieldControl : BaseFieldControl
    {

        public bool LimitToCurrentSite = false;
        protected DropDownList Webs = null;
        protected DropDownList DocLibs = null;
        protected DropDownList Documents = null;
        protected Label ErrorLabel = null;
        protected Label DocumentLinkValue = null;
        protected ImageButton Reset = null;

        const string DefaultTemplate = "DocumentLinkFieldControl";
        
        protected override string DefaultTemplateName
        {

            get
            {
                
                return DefaultTemplate;

            }

        }
        
        
        
        
         
        /// <summary>
        /// On construit les contrôles utilisés.
        /// </summary>
        protected override void CreateChildControls()
        {

            if (Field == null) return;




            base.CreateChildControls();



            if (ControlMode == Microsoft.SharePoint.WebControls.SPControlMode.Display)
                return;

            try
            {
                //On va chercher notre label permettant d'afficher les erreurs
                ErrorLabel = TemplateContainer.FindControl("ErrorLabel") as Label;
                //On pointe sur la liste Webs définie dans le user control
                Webs = TemplateContainer.FindControl("Webs") as DropDownList;                
                Webs.SelectedIndexChanged += new EventHandler(Webs_SelectedIndexChanged);
                //On pointe sur la liste DocLibs définie dans le user control
                DocLibs = TemplateContainer.FindControl("DocLibs") as DropDownList;
                DocLibs.SelectedIndexChanged += new EventHandler(DocLibs_SelectedIndexChanged);
                //On pointe sur la liste Documents définie dans le user control
                Documents = TemplateContainer.FindControl("Documents") as DropDownList;
                Documents.SelectedIndexChanged += new EventHandler(Documents_SelectedIndexChanged);
                //On pointe sur le label qui contient le lien relatif vers le document sélectionné
                DocumentLinkValue = TemplateContainer.FindControl("DocumentLinkValue") as Label;
                Reset = TemplateContainer.FindControl("Reset") as ImageButton;

                //Vérification que tous les contrôles ont bien été retrouvés
                if (ErrorLabel == null ||
                    Webs == null ||
                    DocLibs == null ||
                    Documents == null ||
                    DocumentLinkValue == null ||
                    Reset == null)
                {
                    throw new ApplicationException("Failed to initialize controls");
                }
                //Si on édite l'item de liste, on récupère la valeur actuelle
                //de notre colonne stockée dans ItemFieldValue
                if (ItemFieldValue != null)
                    DocumentLinkValue.Text = ItemFieldValue.ToString();
                //Reset est l'image permettant de remettre le lien à blanc
                Reset.Click += new ImageClickEventHandler(Reset_Click);
                //Si on limite au site courant
                if (LimitToCurrentSite)
                {
                    //On ajoute le titre du site courant et on
                    //verrouille la liste
                    Webs.Items.Add(SPContext.Current.Web.Title);
                    Webs.Enabled = false;
                    //On va rechercher les librairies du site courant
                    GetDocLibs(SPContext.Current.Web);
                }
                else
                {
                    //On ajoute une entrée bidon pour forcer l'utilisateur
                    //à faire un choix qui délenche le onchange
                    Webs.Items.Add("Choose...");
                    //On va rechercher tous les sous-sites du site courant
                    //Fonctionnera pour toute la collection si on se trouve
                    //dans le top level site.
                    GetSubWeb(SPContext.Current.Web);
                }
            }                  
                
            catch(Exception Ex)
            {
                //On tente d'afficher l'erreur si possible
                if (ErrorLabel != null)
                    ErrorLabel.Text = Ex.Message;
                else
                    //Sinon on lance une app exception qui sera 
                    //catchée par SP
                    throw new ApplicationException(Ex.Message);
            }

        }
        //Remet la valeur de la colonne à blanc
        void Reset_Click(object sender, ImageClickEventArgs e)
        {
            DocumentLinkValue.Text = "";
        }
        //Retourne la valeur du label servant à stocker
        //la valeur du lien
        public override object Value
        {
            get
            {                
                EnsureChildControls();
                return DocumentLinkValue.Text;
            }        

        }       
        
        //Met la valeur du lien.
        void Documents_SelectedIndexChanged(object sender, EventArgs e)
        {
            if (Documents.SelectedIndex != 0)
            {
                DocumentLinkValue.Text = Documents.SelectedValue;                
            }
            else
            {
                DocumentLinkValue.Text = "";
            }
            
        }
        //Si on a choisi une doc lib, on va rechercher les documents de
        //celle-ci
        void DocLibs_SelectedIndexChanged(object sender, EventArgs e)
        {
            if (DocLibs.SelectedIndex != 0)
            {
                if (LimitToCurrentSite)
                {
                    //On passe le site courant
                    GetDocs(SPContext.Current.Web.Lists[
                        new Guid(DocLibs.SelectedValue)]);
                }
                else
                {
                    //On passe le site sélectionné
                    GetDocs(
                        new SPSite(
                            Webs.SelectedValue).OpenWeb().Lists[
                                new Guid(DocLibs.SelectedValue)]);
                }
            }
        }
        //Lorsqu'on a choisi un site, on va rechercher toutes ses bibliothèques
        void Webs_SelectedIndexChanged(object sender, EventArgs e)
        {
            if (Webs.SelectedIndex != 0)
            {
                GetDocLibs(new SPSite(Webs.SelectedValue).OpenWeb());
            }
            else
            {
                DocLibs.Items.Clear();
            }
        }
        
        //méthode récursive qui va chercher tous
        //les sous-sites du site courant
        private void GetSubWeb(SPWeb Web)
        {
            ListItem WebItem = null;
            if (Web == SPContext.Current.Web)
            {
                WebItem = new ListItem();
                WebItem.Text = Web.Title;
                WebItem.Value = Web.Url;
                Webs.Items.Add(WebItem);
            }
            foreach (SPWeb W in Web.GetSubwebsForCurrentUser())
            {
                WebItem = new ListItem();
                WebItem.Text = W.Title;
                WebItem.Value = W.Url;
                Webs.Items.Add(WebItem);
               
                if (W.Webs.Count > 0)
                {
                    GetSubWeb(W);
                }
                //Il est important de libérer la mémoire
                //dans ce genre d'opération
                W.Dispose();

            }
            Web.Dispose();
        }

        

        private void GetDocLibs(SPWeb Web)
        {
            DocLibs.Items.Clear();
            DocLibs.Items.Add("Chose...");
            foreach (SPList DocLib in Web.Lists)
            {
                //On limite aux bibliothèques et on affiche pas
                //les bibliothèques cachées
                if (DocLib.BaseType == SPBaseType.DocumentLibrary &&
                    !DocLib.Hidden)
                {
                    ListItem DocLibItem = new ListItem();
                    DocLibItem.Text = DocLib.Title;
                    DocLibItem.Value = DocLib.ID.ToString();
                    DocLibs.Items.Add(DocLibItem);
                }
                    
            }
            Web.Dispose();
        }

        private void GetDocs(SPList DocLib)
        {
            //on va rechercher tous les documents de la bibliothèque
            //sur un seul niveau (pas de gestion de répertoire).
            Documents.Items.Clear();
            Documents.Items.Add("Choose..");
            SPQuery Query = new SPQuery();
            Query.ViewFields = "<FieldRef Name='LinkFilename' />";
            foreach (SPListItem Doc in DocLib.GetItems(Query))
            {
                ListItem DocItem = new ListItem();
                DocItem.Value = DocLib.ParentWebUrl+"/"+Doc.Url;
                DocItem.Text = Doc["Name"].ToString();
                Documents.Items.Add(DocItem);
            }
        }



        
    }
}
Et le code du contrôle utilisateur DocumentLink.ascx qui est lié

<%@ Control Language="C#" Debug=true  %>

<%@Assembly Name="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>

<%@Register TagPrefix="SharePoint" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"
 namespace="Microsoft.SharePoint.WebControls"%>

 

<SharePoint:RenderingTemplate ID="DocumentLinkFieldControl" runat="server">
    <Template>                
        <div><b>Site:</b></div><asp:DropDownList ID="Webs" BackColor="orange"  AutoPostBack="true" runat="server" /><br />
        <div><b>Library:</b></div><asp:DropDownList ID="DocLibs" BackColor="orange" AutoPostBack="true" runat="server" /><br />
        <div><b>Document:</b></div><asp:DropDownList ID="Documents" BackColor="orange" runat="server" AutoPostBack="true" />   <br />     
        <div><b>Document Link:</b></div><asp:Label ID="DocumentLinkValue" runat="server" /><asp:ImageButton ID="Reset" 
		                  ImageUrl="/_layouts/images/restore.gif" runat="server"/>
        <asp:Label ID="ErrorLabel" ForeColor="red" runat="server" />
    </Template>
</SharePoint:RenderingTemplate>
Les points importants à retenir sont:

  • On dérive de BaseFieldControl
  • On précise que le template à utiliser est DocumentLinkFieldControl, ceci permet au système de retrouver notre template défini dans le contrôle utilisateur
  • La méthode CreateChildControls dans laquelle on fait pointer tous nos membres vers les contrôles définis dans le contrôle utilisateur DocumentLink.ascx
  • Les diverses méthodes permettant de récupérer les sites, les bibliothèques et les documents. Les commentaires dans le code sont suffisants.

IV-C. Le code gérant notre propriété personnelle


using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;
using System.Data;

namespace DocumentLink
{
    public class DocumentProperties : UserControl, IFieldEditor
    {

        protected CheckBox LimitToCurrentSiteChk;

        private bool value;

        

        public void InitializeWithField(SPField field)
        {

            DocumentLnk DocumentLinkField = field as DocumentLnk;

            if (DocumentLinkField != null)
                this.value = DocumentLinkField.LimitToCurrentSite;
            
        }
        

        public void OnSaveChange(SPField field, bool isNew)
        {

            //On pointe sur notre colonne personnelle
            DocumentLnk DocumentLinkField = field as DocumentLnk;           
            //Si la colonne est ajoutée à la liste
            if (isNew)
                DocumentLinkField.UpdatePropMethod(this.LimitToCurrentSiteChk.Checked);
            //Si la colonne est modifiée
            else
                DocumentLinkField.LimitToCurrentSite = this.LimitToCurrentSiteChk.Checked;
            

        }
        //En général on la met à oui. Ceci permet simplement
        //d'isoler notre contrôle dans une autre section
        public bool DisplayAsNewSection
        {
            get
            {
                return true;
            }
        }
        //On affiche la valeur courant de la propriété
        protected override void CreateChildControls()
        {
            base.CreateChildControls();
            LimitToCurrentSiteChk.Checked = this.value;
            
        }


        
    }


}
Et le contrôle utilisateur DocumentLinkEditorControl.ascx qui est lié à ce code

<%@ Control Language="C#" 
   Inherits="DocumentLink.DocumentProperties,DocumentLink, Version=1.0.0.0, Culture=neutral, PublicKeyToken=739434e87c9ca6ae"   
   AutoEventWireup="false" compilationMode="Always" %>

<%@ Register TagPrefix="wssuc" TagName="InputFormControl" src="~/_controltemplates/InputFormControl.ascx" %>

<%@ Register TagPrefix="wssuc" TagName="InputFormSection" src="~/_controltemplates/InputFormSection.ascx" %>

<%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls"
   Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
 <%@ Register Tagprefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" 
   Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> 
 <%@ Import Namespace="Microsoft.SharePoint" %> 

<wssuc:InputFormSection runat="server" id="LimitToCurrentSite" Title="Scope">

       <Template_InputFormControls>

             <wssuc:InputFormControl runat="server"

                    LabelText="Limit To Current Site">

                    <Template_Control>

                           <asp:CheckBox id="LimitToCurrentSiteChk" runat="server">

                           </asp:CheckBox>

                    </Template_Control>

             </wssuc:InputFormControl>

       </Template_InputFormControls>

</wssuc:InputFormSection>
Les points importants à retenir sont:

  • Ce code est "branché" par le contrôle utilisateur DocumentLinkEditorControl.ascx via l'attribut Inherits de la directive Control
  • On dérive de UserControl et on implémente IFieldEditor
  • On implémente les méthodes InitializeWithField et OnSaveChange ainsi que la propriété DisplayAsNewSection qui viennent de l'interface IFieldEditor
Tout ceci paraît beaucoup pour gérer une simple checkbox qui nous indique si oui ou non l'utilisateur souhaite limiter la zone au site courant. C'est en effet assez costaud comme code mais après avoir écumé tout le web et après avoir essayé énormément d'autres possibilités, c'est la seule technique qui semble parfaitement fiable. L'utilisation d'un Dictionary pour stocker la valeur plutôt que d'utiliser setCustomProperty directement est une astuce que j'ai trouvé sur un forum américain et faisait l'unanimité car tout le monde avait déjà plus ou moins tout essayé.


VI. Le fichier FLDTYPES_xxx.xml décrivant le comportement de notre colonne


VI-A. La définition de la colonne proprement dite

Pour que Sharepoint soit en mesure d'afficher notre colonne dans la liste des colonnes ajoutables à une liste, nous devons décrire celle-ci au travers de ce fichier XML

La définition proprement dite se limite à quelques lignes

<Field Name="TypeName">DocumentLink</Field>
<Field Name="InternalType">Text</Field>
<Field Name="ParentType">Text</Field>
<Field Name="TypeDisplayName">Link to a document</Field>
<Field Name="TypeShortDescription">Link to a document</Field>
<Field Name="UserCreatable">TRUE</Field>
<Field Name="ShowInListCreate">TRUE</Field