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.
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
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/">
<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" />
<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>
<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}&List={ListId}&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}&List={ListId}&Source=' + window.location"/>
</CustomAction>
<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}&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
<FieldTypes>
<FieldType>
<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>
<RenderPattern Name="DisplayPattern">
<Switch>
<Expr>
<Column />
</Expr>
<Case Value="1">
<HTML><![CDATA[]]></HTML>
<HttpVDir/>
<HTML><![CDATA[]]></HTML>
<Column Name="ID" HTMLEncode="TRUE"/>
<HTML><![CDATA[]]></HTML>
<ListProperty Select='Name'/>
<HTML><![CDATA[]]></HTML>
<HTML><![CDATA[]]></HTML>
</Case>
<Case Value="2">
<HTML><![CDATA[]]></HTML>
<HttpVDir/>
<HTML><![CDATA[]]></HTML>
<Column Name="ID" HTMLEncode="TRUE"/>
<HTML><![CDATA[]]></HTML>
<ListProperty Select='Name'/>
<HTML><![CDATA[]]></HTML>
<HTML><![CDATA[]]></HTML>
</Case>
<Case Value="3">
<HTML><![CDATA[]]></HTML>
<HttpVDir/>
<HTML><![CDATA[]]></HTML>
<Column Name="ID" HTMLEncode="TRUE"/>
<HTML><![CDATA[]]></HTML>
<ListProperty Select='Name'/>
<HTML><![CDATA[]]></HTML>
<HTML><![CDATA[]]></HTML>
</Case>
<Case Value="4">
<HTML><![CDATA[]]></HTML>
<HttpVDir/>
<HTML><![CDATA[]]></HTML>
<Column Name="ID" HTMLEncode="TRUE"/>
<HTML><![CDATA[]]></HTML>
<ListProperty Select='Name'/>
<HTML><![CDATA[]]></HTML>
<HTML><![CDATA[]]></HTML>
</Case>
<Case Value="5">
<HTML><![CDATA[]]></HTML>
<HttpVDir/>
<HTML><![CDATA[]]></HTML>
<Column Name="ID" HTMLEncode="TRUE"/>
<HTML><![CDATA[]]></HTML>
<ListProperty Select='Name'/>
<HTML><![CDATA[]]></HTML>
<HTML><![CDATA[]]></HTML>
</Case>
<Default>
<IfEqual>
<Expr1>
<Field Name="ContentType" />
</Expr1>
<Expr2>
Document Rating
</Expr2>
<Then>
<HTML><![CDATA[]]></HTML>
<HttpVDir/>
<HTML><![CDATA[]]></HTML>
<Column Name="ID" HTMLEncode="TRUE"/>
<HTML><![CDATA[]]></HTML>
<ListProperty Select='Name'/>
<HTML><![CDATA[]]></HTML>
<HTML><![CDATA[]]></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()
{
using (SPSite Site = new SPSite(SPContext.Current.Web.Url))
{
using (SPWeb Web = Site.OpenWeb())
{
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"]));
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;
private string GetString(byte[] InputStream)
{
UTF8Encoding Encoding = new UTF8Encoding();
return Encoding.GetString(InputStream).Substring(1);
}
private bool UserVoteEntry(UserVote Arg)
{
if (Arg.LoginName == LogArg)
return true;
else
return false;
}
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());
}
public string SerializeUserVote(UserVote CurrentVote)
{
return SerializeUserVote(null, CurrentVote.LoginName, CurrentVote.FullName, CurrentVote.Vote);
}
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);
}
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
|