Développer un système d'évaluation de document pour SharePoint

Dans ce tutoriel, nous allons voir comment on peut développer un système d'évaluation de document, de manière simple.

- Titre : Développer un système d'évaluation de document pour SharePoint

- Parution : 13 juin 2008

- Licence : Copyright ®  Developpez. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Présentation du projet

- II. Structure de notre projet

- III. Fonctionnement du système

- III-A. Structure de notre fichier Elements.xml

- III-B. Comportement de notre colonne affichant le résultat des votes

- III-C. Enregistrement d'un vote

- III-D. Rapport des votes de toute une bibliothèque

- IV. Téléchargement

I - Présentation du projet

Ce projet a pour but de permettre aux visiteurs authentifiés d'évaluer des documents. Cette évaluation permettra d'établir une classification et de mettre en avant les documents/vidéos etc.. les plus appréciés par les visiteurs dans un environnement SharePoint.

La moyenne obtenue pour chaque document est fiable à 100% dans la mesure où seuls les utilisateurs authentifiés peuvent voter et qu'un seul vote pour un document sera pris en compte. Un utilisateur pourra changer son vote s'il le souhaite mais seul son dernier vote compte. On a donc la certitude d'éviter qu'une personne vote 10 fois pour un document juste pour faire monter la classification.

Cependant, le système est assez simpliste mais vous pourrez le faire évoluer puisque les sources seront disponibles (voir section téléchagement).

Voici en quelques captures d'écran, un aperçu du projet.

Le projet met à disposition un nouveau modèle de liste (Document Rating)

Lorsque l'on a créé une instance de ce nouveau type, on obtient une liste dont les documents sont évaluables. La moyenne des votes est symbolisée par des étoiles

Le système de vote est basé sur un type de contenu contenant un custom field et un autre champ caché contenant l'historique des votes pour un document. Vous n'êtes donc pas obligé d'utiliser le modèle de liste Document Rating. Vous pouvez simplement associer le type de contenu Document Rating à n'importe quelle bibliothèque.

Lorsque ce type de contenu est associé à l'un de vos documents, des custom actions sont disponibles pour permettre au visiteur de voter.

Lorsque le visiteur clique sur Vote ou sur les étoiles, il est redirigé vers une page lui permettant d'évaluer le document

Un gestionnaire de liste pourra accéder à deux types de rapports. Un rapport de tous les votes liés à un seul élément

et un rapport reprenant tous les votes pour une bibliothèque entière.

Enfin, grâce à ce système qui stocke simplement une valeur (1,2,3,4 ou 5), on va pouvoir utiliser les vues standard de SharePoint pour établir des tris, des filtres etc...sur nos bibliothèques

II - Structure de notre projet

- Document Rating : Contient la structure de note modèle de liste

- Features : Contient la description de notre feature et les custom actions

- IMAGES : Contient toutes les images du projet

- LAYOUTS : Contient les pages applicatives, à savoir, celle qui permet de voter, le rapport pour un document et le rapport pour toute une librairie

- XML : Le document XML décrivant notre colonne personnelle (étoiles)

- HighLight.cs : Classe principale du custom field dérivant de SPFieldText

- ItemRating.cs, ItemReport.cs et ListRatingReport.cs : code-behind des pages de vote et Rapports

- RatingValues.cs : Contient une énumération des niveaux de votes possibles

- UserVote.cs : Représente un utilisateur et son vote. Les objets créés via cette classe sont ensuite sérialisés en XML et stockés dans un champ caché

- UserVoteXmlHandler.cs : Classe s'occuptant de la sérialisation/désérialisation des votes

Nous retrouvons donc dans notre projet, un custom field, des pages applicatives et des custom actions

III - Fonctionnement du système

- Un utilisateur authentifié peut voter

- Si un utilisateur vote plusieurs fois pour un même document, seul son dernier vote est pris en compte

- L'utilisateur peut voter par le biais d'une custom action dans l'ECB ou en cliquant sur notre custom field (soit sur les étoiles, soit sur un lien explicite)

- Un administrateur de liste peut visualiser le rapport des votes d'un document via une custom action

- Un administrateur de liste peut visualiser le rapport des votes d'une bibliothèque entière via une custom action

- Chaque historique de vote est contenu dans l'élément de liste auquel se rapporte un document, il n'y a donc pas d'historique centralisée des votes

- Tout document peut-être évalué, la seule condition est qu'il ait le type de contenu "Document Rating" qui est lui-même dérivé du type de contenu "Document"

III-A - Structure de notre fichier Elements.xml

Ce fichier sert à déployer notre type de contenu Document Rating et nos custom actions

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<!--The fields below are deployed as site columns-->
<Field Type="Note"
DisplayName="Vote History"
Hidden="TRUE"
Required="FALSE"
UnlimitedLengthInDocumentLibrary="TRUE"
RichText="FALSE"
Sortable="FALSE"
Group="Custom Columns"
ID="{B09C56CD-E2A8-4cc9-940C-2B5DE0CFE3CB}"
SourceID="http://schemas.microsoft.com/sharepoint/v3"
StaticName="RateStorage"
Name="RateStorage"
ColName="ntext2"
RowOrdinal="0" />

<Field Type="HighLight"
DisplayName="Vote Results"
ShowInEditForm="FALSE"
ShowInNewForm="FALSE"
Required="FALSE"
ID="{3432A932-642A-4ba1-B6F2-884316712535}"
SourceID="http://schemas.microsoft.com/sharepoint/v3"
StaticName="RateValue"
Name="RateValue"
ColName="nvarchar11"
RowOrdinal="0" />

<!--This site content type includes the fields defined above-->
<ContentType ID="0x0101001B940DAB6AD6487085FD25BA3A462A9F"
Name="Document Rating"
Description="Allows Users To Rate Documents"
Version="0">
<FieldRefs>
<FieldRef ID="{3432A932-642A-4ba1-B6F2-884316712535}"
DisplayName="Vote Results" Name="RateValue" ShowInEditForm="FALSE"
ShowInNewForm="FALSE" ShowInDisplayForm="TRUE" />
<FieldRef ID="{B09C56CD-E2A8-4cc9-940C-2B5DE0CFE3CB}"
Name="RateStorage" Hidden="TRUE"/>
</FieldRefs>
</ContentType>
<!--
This custom action is shown in the ECB of list items belonging
to the current above content type
-->
<CustomAction
Id="70FCCA5C-B293-4778-8DF1-737F3545E5E1"
Rights="ViewListItems"
RegistrationType="ContentType"
RegistrationId="0x0101001B940DAB6AD6487085FD25BA3A462A9F"
Location="EditControlBlock"
Sequence="2000"
Title="Vote"
>
<UrlAction Url="javascript:window.location
= '{SiteUrl}/_layouts/ItemRating.aspx?id={ItemId}&amp;List={ListId}&amp;Source=' + window.location"/>

</CustomAction>

<CustomAction
Id="69B43F46-778D-4e10-A851-28715D683741"
Rights="ManageLists"
RegistrationType="ContentType"
RegistrationId="0x0101001B940DAB6AD6487085FD25BA3A462A9F"
Location="EditControlBlock"
Sequence="2001"
Title="View Vote Report"
>
<UrlAction Url="javascript:window.location
= '{SiteUrl}/_layouts/ItemReport.aspx?id={ItemId}&amp;List={ListId}&amp;Source=' + window.location"/>

</CustomAction>

<!--This custom action is located in the list settings-->
<CustomAction
Id="6B4A151D-B545-452c-ADE1-9B8FEA355F85"
GroupId="GeneralSettings"
Location="Microsoft.SharePoint.ListEdit"
Sequence="2001"
Title="View List Votes Report"
Description="View List Votes Report"
Rights="ManageLists"
>
<UrlAction Url="javascript:window.location= '{SiteUrl}/_layouts/ListRatingReport.aspx?List={ListId}&amp;Source=' + window.location"/>
</CustomAction>
</Elements>

Ce qu'il faut retenir de ce code

- Nos colonnes sont déployées en hidden pour l'une et en affichage uniquement pour l'autre (ShowInEditForm/NewForm = false)

- Nous déployons un type de contenu basé sur ces deux colonnes

- Notre custom action "Vote" n'apparaît que si l'élément est de type de contenu "Document Rating" et que si le visiteur courant a les droits de lecture sur l'item

- Notre custom action "View Vote Report" n'apparaît que si l'élément est de type de contenu "Document Rating" et que si le visiteur courant a les droits de gérer la liste

- Notre custom action "View List Votes Report" n'apparaît que si le visiteur courant a les droits de gérer la liste

- Toutes nos custom actions pointent respectivement sur l'une des pages applicatives décrites dans la section précédente

III-B - Comportement de notre colonne affichant le résultat des votes

Cette colonne étant un custom field, il faut lui spécifier son comportement en terme d'affichage. C'est le fichier FLDTYPES_xxx.xml qui se charge de cela. Voici celui que j'ai mis en place pour la colonne

<!--
Author
*******
Stéphane Eyskens (http://blogs.ezos.com/blog/sey)

Purpose
********

This file is the XML used to define and render the custom field type "HighLight".
This custom field is used to rate documents and displays a certain number of stars according to
the rate average. In case no rate has been done so far, a link is displayed to the user in order
to give him/her the possibility to rate the document. This link is only presented if the document
belongs to the content type associated with the custom field.
-->
<FieldTypes>
<FieldType>
<!--Defining field's properties-->
<Field Name="TypeName">HighLight</Field>
<Field Name="TypeDisplayName">HighLight</Field>
<Field Name="TypeShortDescription">HighLight</Field>
<Field Name="ParentType">Text</Field>
<Field Name="FieldTypeClass">HighLight.HighLight, HighLight, Version=1.0.0.0, Culture=neutral, PublicKeyToken=a020282aed59311d</Field>
<Field Name="UserCreatable">FALSE</Field>
<!--The part below renders the field in the raw mode-->
<RenderPattern Name="DisplayPattern">
<!--
the below switch statement evalutes the current value. This
value can be from null to 5.
-->
<Switch>
<Expr>
<Column />
</Expr>
<!--Displays 1 star corresponding to the level : poor-->
<Case Value="1">
<HTML><![CDATA[<a style="padding-left:2px;padding-right:12px" href="]]></HTML>
<HttpVDir/>
<HTML><![CDATA[/_layouts/ItemRating.aspx?Id=]]></HTML>
<Column Name="ID" HTMLEncode="TRUE"/>
<HTML><![CDATA[&amp;List=]]></HTML>
<ListProperty Select='Name'/>
<HTML><![CDATA[" OnClick="GoToLink(this);return false;">]]></HTML>
<HTML><![CDATA[<img border="0" title="Poor" src="/_layouts/images/documentrating/rating_1.gif"/></a>]]></HTML>
</Case>
<!--Displays 2 stars corresponding to the level : low-->
<Case Value="2">
<HTML><![CDATA[<a style="padding-left:2px;padding-right:12px" href="]]></HTML>
<HttpVDir/>
<HTML><![CDATA[/_layouts/ItemRating.aspx?Id=]]></HTML>
<Column Name="ID" HTMLEncode="TRUE"/>
<HTML><![CDATA[&amp;List=]]></HTML>
<ListProperty Select='Name'/>
<HTML><![CDATA[" OnClick="GoToLink(this);return false;">]]></HTML>
<HTML><![CDATA[<img border="0" title="Low" src="/_layouts/images/documentrating/rating_2.gif"/></a>]]></HTML>
</Case>
<!--Displays 3 stars corresponding to the level : good-->
<Case Value="3">
<HTML><![CDATA[<a style="padding-left:2px;padding-right:12px" href="]]></HTML>
<HttpVDir/>
<HTML><![CDATA[/_layouts/ItemRating.aspx?Id=]]></HTML>
<Column Name="ID" HTMLEncode="TRUE"/>
<HTML><![CDATA[&amp;List=]]></HTML>
<ListProperty Select='Name'/>
<HTML><![CDATA[" OnClick="GoToLink(this);return false;">]]></HTML>
<HTML><![CDATA[<img border="0" title="Good" src="/_layouts/images/documentrating/rating_3.gif"/></a>]]></HTML>
</Case>
<!--Displays 4 stars corresponding to the level : very good-->
<Case Value="4">
<HTML><![CDATA[<a style="padding-left:2px;padding-right:12px" href="]]></HTML>
<HttpVDir/>
<HTML><![CDATA[/_layouts/ItemRating.aspx?Id=]]></HTML>
<Column Name="ID" HTMLEncode="TRUE"/>
<HTML><![CDATA[&amp;List=]]></HTML>
<ListProperty Select='Name'/>
<HTML><![CDATA[" OnClick="GoToLink(this);return false;">]]></HTML>
<HTML><![CDATA[<img border="0" title="Very Good" src="/_layouts/images/documentrating/rating_4.gif"/></a>]]></HTML>
</Case>
<!--Displays 5 stars corresponding to the level : excellent-->
<Case Value="5">
<HTML><![CDATA[<a style="padding-left:2px;padding-right:12px" href="]]></HTML>
<HttpVDir/>
<HTML><![CDATA[/_layouts/ItemRating.aspx?Id=]]></HTML>
<Column Name="ID" HTMLEncode="TRUE"/>
<HTML><![CDATA[&amp;List=]]></HTML>
<ListProperty Select='Name'/>
<HTML><![CDATA[" OnClick="GoToLink(this);return false;">]]></HTML>
<HTML><![CDATA[<img border="0" title="Excellent" src="/_layouts/images/documentrating/rating_5.gif"/></a>]]></HTML>
</Case>
<!--In case no vote has been done yet-->
<Default>
<IfEqual>
<!--If the value of the content type is document rating-->
<Expr1>
<Field Name="ContentType" />
</Expr1>
<Expr2>
Document Rating
</Expr2>
<Then>
<!--Then display the link redirecting users to the voting page-->
<HTML><![CDATA[<a style="padding-left:2px;padding-right:12px" href="]]></HTML>
<HttpVDir/>
<HTML><![CDATA[/_layouts/ItemRating.aspx?Id=]]></HTML>
<Column Name="ID" HTMLEncode="TRUE"/>
<HTML><![CDATA[&amp;List=]]></HTML>
<ListProperty Select='Name'/>
<HTML><![CDATA[" OnClick="GoToLink(this);return false;">]]></HTML>
<HTML><![CDATA[Vote!</a>]]></HTML>
</Then>
</IfEqual>
</Default>
</Switch>
</RenderPattern>
</FieldType>
</FieldTypes>

Ce qu'il faut retenir de ce code

- un switch se fait pour voir si le champ contient 1,2,3,4 ou 5 correspondant respectivement aux évaluations poor,low,good,high,excellent. En fonction de cette valeur, on affiche 1,2,3,4 ou 5 étoiles sous forme de lien cliquable redirigeant le visiteurs vers la page de votes.

- Si la valeur est nulle, on passe dans la section Default où l'on vérifie si l'élément de liste a bien le type de contenu "Document Rating", auquel cas, on affiche un lien "Vote" car aucun vote n'a encore été réalisé.

III-C - Enregistrement d'un vote

Lorsque l'utilisateur arrive sur la page de vote, il choisit un niveau d'évaluation et clique sur Ok. Voici la portion de code permettant l'enregistrement du vote pour le document.

SPSecurity.RunWithElevatedPrivileges(delegate()
{
//When impersonating, it is required to instanciate a new SPSite
using (SPSite Site = new SPSite(SPContext.Current.Web.Url))
{
using (SPWeb Web = Site.OpenWeb())
{
//Make sure we can update the list.
bool AllowUnsafeUpdates = Web.AllowUnsafeUpdates;
Web.AllowUnsafeUpdates = true;
Guid ListGuid = new Guid(Request.QueryString["List"].ToString());
SPList TargetList = Web.Lists[ListGuid];
uint RateFieldValue = Convert.ToUInt32(RateValue.SelectedItem.Value);
string CurrentLogin = SPContext.Current.Web.CurrentUser.LoginName;
string CurrentLoginName = SPContext.Current.Web.CurrentUser.Name;
string StorageField = TargetList.Fields.GetFieldByInternalName("RateStorage").Title;
string TargetField = TargetList.Fields.GetFieldByInternalName("RateValue").Title;
SPListItem Itm = TargetList.GetItemById(Convert.ToInt16(Request.QueryString["Id"]));
//Making sure that the targetted item does belong to the right content type
//It also makes sure that there is no injection (as we're running with elevated privileges...)
if (Itm.ContentType.Name == ContentType.Text)
{
UserVoteXmlHandler VoteHandler = new UserVoteXmlHandler();
if (Itm[StorageField] != null)
{
List<UserVote> CurrentVoteList = VoteHandler.UnserializeUserVotes(
Itm[StorageField].ToString());
Itm[StorageField] = VoteHandler.SerializeUserVote(
CurrentVoteList,
CurrentLogin,
CurrentLoginName,
RateFieldValue);

Itm[TargetField] = VoteHandler.GetVoteAverage(CurrentVoteList);
}
else
{
Itm[StorageField] = VoteHandler.SerializeUserVote(
new UserVote(CurrentLogin, CurrentLoginName,RateFieldValue));
Itm[TargetField] = RateFieldValue;
}
try
{
Itm.Update();
}
catch (SPException Ex)
{
ShowError(Ex.Message);
}

}
Web.AllowUnsafeUpdates=AllowUnsafeUpdates;
}

}
});

Ce qu'il faut retenir de ce code

- Ce n'est pas montré par ce code mais une vérification préalable se fait pour savoir si l'utilisateur est bien authentifié

- On fait une impersonation en tant qu'application pool car toute personne authentifiée peut voter, le seul rôle requis est "Read" et non pas "Contribute".

- Si le type de contenu est bien conforme, on vérifie si un vote a déjà été émis (tout utilisateur confondu) ou non pour ce document. Si c'est le cas, on récupère l'historique actuelle et on transmet le vote courant sinon on sérialise directement la valeur du vote actuel

- Enfin, on vérifie que l'on obtient pas une exception sur la mise à jour. En effet, ceci pourrait subvenir si deux utilisateurs votent en même temps pour un même document. En fonction du nombre de votes déjà réalisés, le temps de désérialiser/resérialiser les votes et de les enregistrer, peut-être qu'une mise à jour de l'élément se produira...dans ce cas, une exception sera lancée par SharePoint car il détectera que l'élément courant a été modifié depuis qu'on l'a récupéré via un SPListItem et le vote ne sera pas enregisté, ce qui nous garantit une totale cohérence au niveau des votes.

Pour mieux comprendre le mécanisme de sérialisation, voici le code de la classe se chargeant de la sérialisation.

internal class UserVoteXmlHandler
{
MemoryStream MemStr = null;
XmlSerializer VotesSerializer = new XmlSerializer(typeof(List<UserVote>));
XmlTextWriter Writer = null;
string LogArg = null;

//returns a string from a byte array
private string GetString(byte[] InputStream)
{
UTF8Encoding Encoding = new UTF8Encoding();
return Encoding.GetString(InputStream).Substring(1);
}
//Predicate called to check whether the user has already voted
private bool UserVoteEntry(UserVote Arg)
{
if (Arg.LoginName == LogArg)
return true;
else
return false;

}
//serialises all the votes (previous+new one)
public string SerializeUserVote(List<UserVote> CurrentVotes,
string LoginName,string Name,uint UserVoteValue)
{
UserVote NewVote = new UserVote(LoginName, Name, UserVoteValue);
if (CurrentVotes == null)
{
CurrentVotes = new List<UserVote>();
CurrentVotes.Add(NewVote);
}
else
{
LogArg = LoginName;
UserVote CheckEntry = CurrentVotes.Find(UserVoteEntry);
if (CheckEntry == null)
CurrentVotes.Add(NewVote);
else
CheckEntry.Vote = UserVoteValue;
}

MemStr = new MemoryStream();
Writer = new XmlTextWriter(MemStr, Encoding.UTF8);
VotesSerializer.Serialize(Writer, CurrentVotes);
MemStr = (MemoryStream)Writer.BaseStream;
return GetString(MemStr.ToArray());

}
//called when registering only the current vote.
public string SerializeUserVote(UserVote CurrentVote)
{
return SerializeUserVote(null, CurrentVote.LoginName, CurrentVote.FullName, CurrentVote.Vote);
}
//Returns the vote average for a given document
public int GetVoteAverage(List<UserVote> Votes)
{
double Total = 0;
for (int i = 0; i < Votes.Count; i++)
{
Total += Votes[i].Vote;
}
return (int)Math.Round((Double)Total / Votes.Count);
}
//returns all the votes until now
public List<UserVote> UnserializeUserVotes(string VoteData)
{
XmlTextReader Reader = new XmlTextReader(
new StringReader(VoteData));
List<UserVote> UnSerialized = (List<UserVote>)VotesSerializer.Deserialize(Reader);
return UnSerialized;
}

}

Et voici la fameuse classe que l'on sérialise et qui symbolise un vote

[XmlRootAttribute(ElementName = "UserVote", IsNullable = false)]
public class UserVote
{
public UserVote() { }
public UserVote(string UserArg, string NameArg, uint VoteArg)
{
LoginName = UserArg;
FullName = NameArg;
Vote = VoteArg;
}
string _LoginName;

public string LoginName
{

get
{
return _LoginName;
}
set
{
_LoginName = value;
}
}
string _FullName;
public string FullName
{
get
{
return _FullName;
}
set
{
_FullName = value;
}
}
uint _Vote;
public uint Vote
{
get
{
return _Vote;
}
set
{
_Vote = value;
}
}
}

III-D - Rapport des votes de toute une bibliothèque

Le rapport des votes fonctionne selon le même principe, voici une version courte du code générant ce rapport

SPQuery VotingQuery = new SPQuery();
VotingQuery.Query = "<Where><Eq><FieldRef Name=\"ContentType\" /><Value Type=\"Text\">"+ContentType.Text+"</Value></Eq></Where>";
VotingQuery.ViewFields = "<FieldRef Name='RateValue' /><FieldRef Name='RateStorage' /><FieldRef Name='Title' />";
VotingQuery.ViewAttributes = "Scope='Recursive'";
SPListItemCollection Results = TargetList.GetItems(VotingQuery);
if (Results.Count > 0)
{
UserVoteXmlHandler VoteHandler = new UserVoteXmlHandler();
List<UserVote> Votes = null;
string DocName = null;
string DocUrl = null;

foreach (SPListItem Itm in Results)
{
DocName = Itm["Name"].ToString();
TreeNode DocNode = new TreeNode(DocName);
DocNode.Value = TargetList.RootFolder.Url + Itm.Url;
DocUrl = SPContext.Current.Web.Url + "/" + Itm.Url;
DocNode.NavigateUrl =
DocNode.ImageUrl = "/_layouts/images/doclink.gif";
VoteData.Nodes.Add(DocNode);
if(Itm[StorageField]!=null)
{

Votes = VoteHandler.UnserializeUserVotes(
Itm[StorageField].ToString());

for (int i = 0; i < Votes.Count; i++)
{
TreeNode DocChild1 = new TreeNode(string.Format("({0})-{1}",Votes[i].LoginName,Votes[i].FullName));
DocChild1.ImageUrl = "/_layouts/images/PERUSR.GIF";
DocChild1.NavigateUrl = String.Format("/_layouts/userdisp.aspx?ID={0}",
SPContext.Current.Web.AllUsers[Votes[i].LoginName].ID);
TreeNode DocChild2 = new TreeNode(Enum.GetName(typeof(RatingValues.VotingValues),
Votes[i].Vote));
DocChild2.ImageUrl = "/_layouts/images/RECT.GIF";
DocChild2.SelectAction = TreeNodeSelectAction.None;
DocChild1.ChildNodes.Add(DocChild2);
VoteData.Nodes[NodeLine].ChildNodes.Add(DocChild1);

}
}
NodeLine++;
}
}

Ce qu'il faut retenir de ce code

- On oublie pas de spécifier le scope au niveau de la requête CAML afin d'inclure TOUS les documents de la bibliothèque, tous répertoires confondus

- Pour chaque document, on désérialise l'historique et on récupère une liste de UserVote. On parcours les votes et on les ajoute au fur et à mesure à notre treeview

- Pour chaque noeud, on met un lien vers le document et/ou vers l'utilisateur

IV - Téléchargement

Le code source complet du projet ainsi que le fichier de solution sont disponibles sur codeplex à cette adresse