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