So, SharePoint 2010 comes with a lot of cool, new social features, including an option allowing users to rate content. Interesting stuff, as I had a requirement where I would need the possibility for users to rate articles (PDF documents) in a document library. The requirement consisted of having a page which is basically a consolidated view of the article itself and a few related social features, one of which being the social rating function. Whenever a user wishes to read the article, a dialog frame pops up with the aforementioned page.
I opted for using a _layouts
based page which would take the ID of the article as a parameter to display the contents accordingly. Now, since SharePoint 2010 comes with a standard rating function, I figured I should use the out of the box control, to reduce the amount of customization.
The standard control used by SharePoint is the AverageRatingFieldControl
class, residing in the Microsoft.SharePoint.Portal.WebControls
namespace of assembly Microsoft.SharePoint.Portal
.
I started out by adding the required tag prefix registration and by adding the control itself to the main placeholder of the page:
<%@ Register Tagprefix="SharePointPortal" Namespace="Microsoft.SharePoint.Portal.WebControls" Assembly="Microsoft.SharePoint.Portal, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> <SharePointPortal:AverageRatingFieldControl ID="PageRatingControl" FieldName="AverageRating" runat="server" ControlMode="Display" />
This would actually render the control, but I wasn’t able to actually rate the current item, nor would it display the current rating. That makes sense, as it has no way of knowing which item is supposed to be used. Using a disassembler I found out that the control operates under the assumption of apparently the list item context being available. However, as this is a _layouts
-based page, neither the list or list item context is available, so that leaves us with an issue.
I decided to see if I could somehow manually include the script required for the control to work. Disclaimer: The following description feels a bit like a dirty hack, even though I got it to work in the end. The decision whether to actually use this, I leave entirely to you. I do intend to incorporate all of this in a custom web control, so it’ll be easier to use.
First of all, we need to JavaScript code required for the control. Add the following to the PlaceHolderAdditionalPageHead
placeholder:
<script type="text/javascript"> function RatingsLoader_RatingsCtrl_<%= PageRatingControl.ClientID %>() { var ratingsData = new RatingsCommonData('<img src=\'/_layouts/images/loading16.gif\'> Loading...', 'Average Rating', 'Number of Ratings', 'My Rating', 'Thank you. Your rating has been submitted and will be processed soon.', 'Not available', 'Not rated yet', 'Click to assign a rating (0-5)', '', '<%= this.WebId %>', '<%= this.WebUrl %>', '<%= this.SiteId %>', '\u002f_layouts\u002fImages\u002fRatingsNew.png', '\u002f_layouts\u002fImages\u002fRatingsEmpty.png'); var ratingsControl = new RatingsControl('<%= this.ArticlePathEncoded %>', '<%= this.ArticleTitle %>', 'RatingsCtrl_<%= PageRatingControl.ClientID %>', ratingsData); } ExecuteOrDelayUntilScriptLoaded(RatingsLoader_RatingsCtrl_<%= PageRatingControl.ClientID %>, 'ratings.js'); </script> <script src="/_layouts/ratings.js?rev=KT%2FEk3FDXaspEd0aPPHuhQ%3D%3D" type="text/javascript"></script
Note that I’m actually pulling a few properties from the code-behind of the page:
public string SiteId { get { return SPContext.Current.Site.ID.ToString(); } } public string WebId { get { return SPContext.Current.Web.ID.ToString(); } } public string WebUrl { get { return SPHttpUtility.EcmaScriptStringLiteralEncode(SPHttpUtility.UrlPathEncode(SPContext.Current.Web.Url, true, true)); } }
ArticleTitle
, ArticlePath
and ArticlePathEncoded
are properties that respectively contain the values of the document title, full path to the PDF and encoded full path, like this:
ArticleTitle = article.Title; ArticlePath = article.GetFullUrl(SPContext.Current.Web); ArticlePathEncoded = SPHttpUtility.EcmaScriptStringLiteralEncode(SPHttpUtility.UrlPathEncode(ArticlePath, true, true));
When you load the page this time, rating the requested article should actually be possible now. Picking a rating will correctly register the value and it will be picked up the next time the rating synchronization job runs.
However, we’re not there yet. The value of the control is actually pulled from its underlying field type and this value also determines the CSS class which is rendered in the control. Since again we don’t have the corresponding item context, the control has no way of retrieving the required value.
This is were SPServices comes into play. With SPServices, we can utilize jQuery to access SharePoint’s web services, and in this case, the SocialDataService. This service has a method GetRatingAverageOnUrl
, which returns the average social rating for a given URL.
Initially the call from SPServices would result in a service error: “Object reference not set to an instance of an object”. For a solution for this problem, please see my other blog post.
So, to get it to work, include the following JavaScript code after the previously mentioned code block (note: change the paths to the jQuery and SPServices libraries to your locations):
<script src="/_layouts/scripts/jQuery/jquery-1.6.2.min.js" type="text/javascript"></script> <script src="/_layouts/scripts/SPServices/jquery.SPServices-0.6.2.min.js" type="text/javascript"></script> <script type="text/javascript"> $(function () { $().SPServices({ operation: "GetRatingAverageOnUrl", async: false, url: "<%= this.ArticlePath %>", completefunc: function (xData, status) { if(status != 'success') { return; } var result = $(xData.responseXML).find("[nodeName='Average']").first(); var avg = parseFloat(result.text()); $(".ms-currentRating img").addClass(getClassNameForRating(avg)); } }); function getClassNameForRating(rating) { if(isNaN(rating) || rating <= 0) return ""; if (rating < 0.25) return "ms-rating_0"; if (rating < 0.75) return "ms-rating_0_5"; if (rating < 1.25) return "ms-rating_1"; if (rating < 1.75) return "ms-rating_1_5"; if (rating < 2.25) return "ms-rating_2"; if (rating < 2.75) return "ms-rating_2_5"; if (rating < 3.25) return "ms-rating_3"; if (rating < 3.75) return "ms-rating_3_5"; if (rating < 4.25) return "ms-rating_4"; if (rating < 4.75) return "ms-rating_4_5"; else return "ms-rating_5"; } }); </script>
ArticlePath
is again the property defined in the code-behind, like mentioned earlier. This should be the full path to the document. The value we need comes with the response XML of the web service call, in an element called <Average />
.
The method getClassNameForRating(rating)
takes a float value as a parameter and returns the corresponding CSS class. I actually pulled this code from the rating web control using a disassembler.
The last step now is to set the CSS class to the corresponding rating image, which is defined by the $(".ms-currentrating img")
selector.
Reload the page and you should now see the appropriate amount of stars being displayed by the control!
The end result is a social panel like this:
Great post!
Is it possible to display a numerical value at the end of the five stars to display how many users have rated?
By the looks of it, there is a method on the SocialDataService called CountRatingsOnUrl that takes a URL as argument and then returns the number of ratings. See this page for details: http://msdn.microsoft.com/en-us/library/websvcsocialdataservice.socialdataservice.countratingsonurl.aspx
So yeah, taking the results of that call and then with jQuery, it should be possible to include an additional element (with the rating count) after the element that contains the number of ratings.
Great post! Can you make the entire solution downloadable (ie. the aspx and CodeBehind)?
Not sure, I’d have to look for it, since it was something I did on an earlier project. I’ll get back to you!