Les colonnes personnelles (custom field) avec Sharepoint

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.

- Titre : Les colonnes personnelles (custom field) avec Sharepoint

- Parution : 28 janvier 2008

- Licence : Ce document est issu de https://www.developpez.com et reste la propriété exclusive de son auteur. La copie, modification et/ou distribution par quelque moyen que ce soit est soumise à l'obtention préalable de l'autorisation de l'auteur.

Sommaire

- 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é

- VII. 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="ShowOnListCreate">TRUE</Field>
<Field Name="ShowOnSurveyCreate">TRUE</Field>
<Field Name="ShowOnDocumentLibraryCreate">TRUE</Field>
<Field Name="ShowOnColumnTemplateCreate">TRUE</Field>
<Field Name="FieldTypeClass">
DocumentLink.DocumentLnk,DocumentLink, Version=1.0.0.0, Culture=neutral, PublicKeyToken=739434e87c9ca6ae
</Field>
<Field Name="FieldEditorUserControl">/_controltemplates/DocumentLinkEditorControl.ascx</Field>

On constate que on définit le type de la colonne, sa description, sa présence au niveau de l'addition des colonnes dans des listes, des bibliothèque etc...

On définit également la signature de notre assemblage dans l'attribut FieldTypeClass. Enfin, on notifie l'utilisation d'un contrôle utilisateur pour gérer l'édition des propriétés de notre colonne via l'attribut FieldEditorUserControl.

VI-B - Le DisplayPattern

L'affichage en mode liste est représenté par la figure 5 en section 2. La seule manière d'intervenir sur cet affichage est de créer une section spécifique dans le fichier FLDTYPES_xxx.xml. Dans notre projet ce fichier s'appelle FLDTYPES_DocumentLink.xml. Le nom de ce fichier doit impérativement commencer par FLDTYPES_ sans quoi votre colonne personnelle n'apparaîtra pas dans la liste des colonnes utilisables dans les listes.

<RenderPattern Name="DisplayPattern">
<Switch>
<Expr>
<Column />
</Expr>
<Case Value=""/>
<Default>
<HTML>
<![CDATA[<a href="]]>
</HTML>
<Column HTMLEncode="TRUE"/>
<HTML>
<![CDATA["><img border="0" src="/_layouts/images/doclink.gif"/></a>]]>
</HTML>
</Default>
</Switch>
</RenderPattern>

Ici on teste simplement si la valeur de notre colonne représentée par Column est vide ou non. Au cas où elle est vide, on affiche rien sinon on génère le HTML nécessaire pour créer une image cliquable pointant sur l'adresse du document sélectionné.

Le displaypattern mériterait probablement un tutoriel à lui tout seul, le meilleur conseil que je puisse vous donner est de vous inspirer de ce qui existe en standard (par exemple dans le fichier FLDTYPES.XML) pour explorer les différentes possibilités de ce pattern.

VI-C - Autres patterns

Pattern

Description

HeaderPattern

Permet de spécifier le comportement de la colonne au niveau de l'en-tête. Exemple : tri, filtre etc..

EditPattern

Permet de gérer l'affichage en mode édition

NewPattern

Permet de gérer l'affichage en mode insertion

PreviewDisplayPattern, PreviewEditPattern et PreviewNewPattern

Permet de définir une prévisualisation pour les éditeurs WYSIWYG

VI-D - La propriété

La propriété LimitToCurrentSite est définie comme suit dans le fichier XML

<PropertySchema>
<Fields>
<Field Name="LimitToCurrentSite" Type="Boolean" Hidden="TRUE">
</Field>
</Fields>
</PropertySchema>

Elle est mise en Hidden="TRUE" car nous l'avons géré via un contrôle utilisateur.

VII - Déploiement

Avec Visual Studio 2005, on peut utiliser le modèle de projet Sharepoint => Empty et ensuite ajouter une colonne personnelle en cliquant sur son projet => Add Items => Field Control. Ensuite en implémentant nos différentes classes, on pourra déployer notre contrôle en exécutant l'option Deploy de Visual Studio.

Le seul problème avec cette technique est qu'elle masque le fichier FLDTYPES_xxx et qu'elle donne d'ailleurs un GUID comme nom aux divers fichiers déployés, ce qui n'est pas très réaliste dans un environnement de production car les noms ne sont pas suffisament explicites.

Pour ces raisons, je préfère opter pour un simple projet de type "Project Library" auquel je fais ajouter les références requises Microsoft.Sharepoint et System.Web et je vais ensuite simplement recréer l'arborescence dans laquelle mes composants seront déployés sur les front-end Sharepoint.

Ceci impose de connaître la structure des répertoires Sharepoint et l'endroit où chaque type de composant est déployé. Je ne vais pas passer en revue tous ces répertoires mais nous allons nénamoins étudier les principaux répertoires

Voici donc le détail sur une installation anglaise, désolé mais je n'ai que des environnements anglais :).

Le répertoire principal est C:\Program Files\Common Files\Microsoft Shared\web server extensions\12 dans lequel on retrouve principalement ceci

ISAPI

Contient toutes les DLL Sharepoint ainsi que tous les services webs

LOGS

Contient tous les fichiers logs de Sharepoint. Très intéressant en cas de problème

CONFIG

Contient toutes sortes de fichiers de configuration dont les fichiers de trust (minimal, medium, custom)

BIN

Contient toute une série d'utilitaires dont le fameux stsadm

Resources

Contient des fichiers de langues utilisés notamment par les features et le CAML en général

TEMPLATE

Voir le détail ci-après

Voici à présent les principaux sous-répertoires de TEMPLATE qui sont du plus grand intérêt pour un développeur Sharepoint

CONTROLTEMPLATES

Contient tous les contrôles utilisateurs utilisés par le système. Dans notre exemple, nous utilisons d'ailleurs deux contrôles qui doivent y être déployés

FEATURES

Contient toutes les fonctionnalités disponibles sur un front-end quelle que soit leur portée. Chaque fonctionnalité sera hébergée dans un sous-répertoire propre

IMAGES

Toutes les images stockées à cet emplacement seront disponibles via l'url /_layouts/images/xxx.gif

LAYOUTS

Contient principalement toutes les pages applicatives

SiteTemplates

Contient toutes les définitions de sites du front-end

XML

Contient notamment tous les fichiers décrivant les colonnes disponibles sur un front-end.

Lorsque l'on connaît ces informations, on sait où nos composants (fichiers XML, contrôles utilisateurs, description de feature etc...) doivent être déployés. On peut donc construire une arborescence similaire directement au sein de notre projet (comme affiché en section III). Ensuite, on peut soit créer sa solution (.wsp) à la main, ce qui peut-être nécessaire pour des composants particuliers, soit utiliser un très bon outil gratuit qui s'appelle WSPBUILDER qui crée le fichier .wsp et le manifest.xml qui va avec.

Pour ce projet, j'ai opté pour la deuxième solution. Ceci nous amène donc à créer un fichier .bat qui sera exécuté à chaque build du projet (Properties -> Build => Exécuter GenerateSolution.bat dans le post-build). Voici donc le contenu de ce fichier

REM le répertoire GAC siginifie à WSPBuilder que la DLL de notre projet doit être
REM déployée en GAC
copy bin\debug\DocumentLink.dll DocumentLink\GAC
cd DocumentLink
REM on appelle WSPBuilder qui va générer DocumentLink.wsp
WSPBuilder.exe
REM on rétracte la solution (si elle a déjà été déployée...sinon ça pose pas de pb)
stsadm -o retractsolution -name DocumentLink.wsp -local -url http://sey-pc/
REM on supprime la solution
stsadm -o deletesolution -name DocumentLink.wsp
REM on ajoute la solution
stsadm -o addsolution -filename DocumentLink.wsp
REM on déploie la solution
stsadm -o deploysolution -name DocumentLink.wsp -allowgacdeployment -local -url http://sey-pc/

Notez que vous devez bien sûr adapter l'URL à votre environnement. J'ai inclus WSPBuilder dans le répertoire DocumentLink faisait partie de l'arborescence de mon projet. Vous trouverez donc cet outil dans l'archive compressée disponible en téléchargement.

VIII - Téléchargement

Le projet est disponible ici

Instructions pour tester l'exemple

- Décompressez l'archive

- Ouvrez la solution avec Visual Studio, éditez GenerateSolution.bat et remplacez l'URL http://sey-pc/ par l'url de votre serveur

- Faites un build de la solution.

Vous pouvez également opter pour cette alternative

- Décompressez l'archive

- Editez directement GenerateSolution.bat, mettez toutes les lignes en commentaire sauf stsadm -o addsolution et -o deploysolution et remplacez l'URL http://sey-pc par la vôtre

- Exécutez GenerateSolution.bat