One of the most requested features for Sitefinity 4 has recently been a rating control for blogs (and other content). This is the reason why I decided to show you in this post how to make one yourself. However, this post is not only about how to integrate a RadRating control with Content modules, but also how to make use of WCF Restful services, hosted in Sitefinity. In the guide, there are also various other know-hows, including - getting the value of a custom field from a template, using jquery ajax in templates and gathering data from an external resource. In the example you will also see how you can make use of Entity Framework with Sitefinity.
First, I will briefly explain the plan. Here are the steps that we are going to take in order to make a working RadRating control integrated with Blog Posts:
1) Creating an extra table inside Sitefinity's database. This table will hold information about the rating.
2) Creating the model of the rating and preparing its CRUD operations inside a RatingManager.
3) Creating a WCF Restful service that makes the connection between the manager and the template.
4) Creating a template and consuming the service from inside it.
As a side feature, I have shown how to put the rating data not only in the database but also in custom fields inside our Blog Posts, so that the rating information can be seen from the backend. In addition to that, I have binded the value of the custom fields inside our template.
So, let's start this step by step:
1)Creating the database table
There isn't much to explain here. It's just a manual adding of a table in Sitefinity's database. The easiest way would be using SQL Management Studio. Our table needs to contain the following:
Two columns that will hold the ID of the content item (blog post in this case) and the ID of the user that voted for this item. This way we will always be able to keep track of who voted for what and prevent double voting. These columns are of type uniqueidentifier and are primary keys.
I also added a column with the rating of the user (I called it Count). This column is of type decimal with 1 symbol after the decimal point (18,1):
Before we proceed with the next step, we must create a new project in our solution that will hold the model, the CRUD operations and the WCF service. Here's a picture of my folder structure:
2) For the model, I simply used Entity Framework, just to show you that it can be easily worked with in Sitefinity. However, you are free to use Open Access or whatever else you like. Creating the model goes the old-fashioned way (right-click on the DataModel folder, Add New Item >> ADO.NET Entity Data Model and then just go through the steps of creating your model). We should build the application here. An App.Config with the connection string to the database will be created inside your project. However, you won't need that. We already said that the service will be hosted inside Sitefinity, so you have to go to the app.config and copy the connection string inside Sitefinity's web.config (that goes directly before <system.web>):
<
connectionStrings
>
<
add
name
=
"First43DBEntities"
connectionString
=
"metadata=res://*/DataModel.RatingModel.csdl|res://*/DataModel.RatingModel.ssdl|res://*/DataModel.RatingModel.msl;provider=System.Data.SqlClient;provider connection string="data source=.\MSSQLR2;initial catalog=First43DB;integrated security=SSPI;multipleactiveresultsets=True;App=EntityFramework""
providerName
=
"System.Data.EntityClient"
/>
</
connectionStrings
>
Now comes the long part with the CRUD operations or the RatingManager. There are methods for Creating, Updating rating and Finding rating by UserId and ContentID (they must be both in the search method because this is a many-to-many relationship). Also here is the method that calculates the total rating of an item and a method that adds the two custom fields:
using
System;
using
System.Collections.Generic;
using
System.Linq;
using
System.Data.Entity;
using
System.Text;
using
RatingControl.DataModel;
using
Telerik.Sitefinity.Modules.Blogs;
namespace
RatingControl.DataManager
{
class
RatingManager
{
public
decimal
GetCurrentRatingOfItem(Guid contentItemId)
{
decimal
sum = 0;
decimal
result = 0;
List<sf_rating> controlsCount =
new
List<sf_rating>();
using
(First43DBEntities context =
new
First43DBEntities())
{
controlsCount = context.sf_rating.Where(r => r.ItemId == contentItemId).ToList();
}
if
(controlsCount.Count == 0)
{
return
-1;
}
if
(controlsCount.Count > 0)
{
foreach
(var item
in
controlsCount)
{
sum += item.Count;
}
result = sum / controlsCount.Count;
}
return
result;
}
public
void
UpdateRating(Guid userID,
decimal
newCount, Guid itemId)
{
using
(First43DBEntities context =
new
First43DBEntities())
{
sf_rating currentRating = FindRatingByItemIDAndUserID(userID, itemId);
currentRating.Count = newCount;
context.Attach(currentRating);
context.ObjectStateManager.ChangeObjectState(currentRating, System.Data.EntityState.Modified);
context.SaveChanges();
}
}
public
sf_rating FindRatingByItemIDAndUserID(Guid userID, Guid itemId)
{
sf_rating result =
new
sf_rating();
using
(First43DBEntities context =
new
First43DBEntities())
{
result = context.sf_rating.Where(r => r.UserId.Equals(userID) && r.ItemId.Equals(itemId)).FirstOrDefault();
}
return
result;
}
public
Guid CreateRating(Guid itemId, Guid userID,
decimal
count)
{
var ratingUserId = Guid.Empty;
using
(First43DBEntities context =
new
First43DBEntities())
{
sf_rating rating =
new
sf_rating();
rating.Count = count;
rating.ItemId = itemId;
rating.UserId = userID;
context.sf_rating.AddObject(rating);
context.SaveChanges();
ratingUserId = rating.UserId;
}
return
ratingUserId;
}
}
}
3) We are ready with the model, so now proceeding to the WCF service. Beforehand, we will go to Blogs >> BlogPosts >> Custom fields for posts and Create three new custom fields of type TextField, which will be used to display the rating information in the backend. I called them RatingSum (which is the total sum of all the ratings), RatingCount which is the number of users that voted and RatingResult which is Sum/Count.
So, back to the creation of the WCF service. For that purpose, I created an interface and a class that implements it. These are actually the members of the services, however we will not need a .svc file, because the service will later on be hosted inside Sitefinity.
Firstly, we create the IRating interface. Most of this is just trivial code that is always used when creating WCF services, the method is POST, format is JSon, body style is Wrapped, because we will use serialized objects with multiple properties for request-response. The arguments that the service will take from the client are the content item ID and the current rating of the user, the returning object will contain all information that the Rating model contains, plus the RatingResult, however we will not make use of it in this example (I will explain more later)
using
System;
using
System.Collections.Generic;
using
System.Linq;
using
System.Text;
using
System.ServiceModel;
using
System.ServiceModel.Web;
using
System.Runtime.Serialization;
using
Telerik.Sitefinity.Utilities.MS.ServiceModel.Web;
using
Telerik.Sitefinity.Web.Services;
namespace
RatingControl.Service
{
[ServiceContract]
interface
IRatingService
{
[WebHelp(Comment =
"Requests the rating sum, passing content item ID and current user rating if such"
)]
[WebInvoke(Method =
"POST"
, UriTemplate =
"/RequestRating"
, ResponseFormat = WebMessageFormat.Json, BodyStyle=WebMessageBodyStyle.Wrapped)]
[OperationContract]
RatingSerialized RequestRating(
string
itemID,
string
userRating);
}
[DataContract]
public
class
RatingSerialized
{
[DataMember]
public
Guid RatingId {
get
;
set
; }
[DataMember]
public
decimal
RateSum {
get
;
set
; }
[DataMember]
public
decimal
UserRate {
get
;
set
; }
[DataMember]
public
Guid ItemId {
get
;
set
; }
[DataMember]
public
Guid UserID {
get
;
set
; }
}
}
Now the Rating.cs class that implements the service method. Here you can see the methods that Update the custom fields of the BlogPost. One of them is for newly created ratings (for users that have not rated already) and the other one is for updating a user's vote. An important part of this code is that line:
blgManager.Provider.SuppressSecurityChecks =
true
;
using
System.Text;
using
System.ServiceModel;
using
System.ServiceModel.Activation;
using
RatingControl.DataModel;
using
RatingControl.DataManager;
using
Telerik.Sitefinity.Security;
using
Telerik.Sitefinity.Blogs.Model;
using
Telerik.Sitefinity;
using
Telerik.Sitefinity.Model;
using
Telerik.Sitefinity.Modules.Blogs;
namespace
RatingControl.Service
{
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
class
RatingService : IRatingService
{
private
static
RatingManager manager =
new
RatingManager();
public
RatingSerialized RequestRating(
string
itemID,
string
userRating)
{
Guid itemIdGuid = Guid.Parse(itemID);
decimal
userRatingParsed =
decimal
.Parse(userRating);
Guid currentUserId = SecurityManager.GetCurrentUser().UserId;
RatingSerialized serialized =
new
RatingSerialized();
if
(currentUserId != Guid.Empty)
{
sf_rating rating = manager.FindRatingByItemIDAndUserID(currentUserId, itemIdGuid);
if
(rating ==
null
)
{
manager.CreateRating(itemIdGuid, currentUserId, userRatingParsed);
rating = manager.FindRatingByItemIDAndUserID(currentUserId, itemIdGuid);
AddBlogFieldsValue(itemIdGuid, userRatingParsed);
}
else
{
UpdateBlogFieldsValue(itemIdGuid, userRatingParsed, rating.Count);
manager.UpdateRating(currentUserId, userRatingParsed, itemIdGuid);
}
serialized = TransformToSerialized(rating, manager.GetCurrentRatingOfItem(itemIdGuid));
}
else
{
serialized = TransformToSerialized(
null
, manager.GetCurrentRatingOfItem(itemIdGuid));
}
return
serialized;
}
private
RatingSerialized TransformToSerialized(sf_rating rating,
decimal
sum)
{
RatingSerialized serialized =
new
RatingSerialized();
if
(rating !=
null
)
{
serialized.ItemId = rating.ItemId;
serialized.UserID = rating.UserId;
serialized.UserRate = rating.Count;
}
serialized.RateSum = sum;
return
serialized;
}
public
void
UpdateBlogFieldsValue(Guid blogPostId,
decimal
ratingValue,
decimal
oldValue)
{
BlogsManager blgManager = BlogsManager.GetManager();
bool
securityChecksSettings = blgManager.Provider.SuppressSecurityChecks;
blgManager.Provider.SuppressSecurityChecks =
true
;
var post1 = blgManager.GetBlogPost(blogPostId);
var master = blgManager.GetBlogPost(post1.OriginalContentId);
decimal
ratingSum =
decimal
.Parse(DataExtensions.GetValue<
string
>(master,
"RatingSum"
));
int
ratingCount =
int
.Parse(DataExtensions.GetValue<
string
>(master,
"RatingCount"
));
ratingSum -= oldValue;
ratingSum += ratingValue;
decimal
ratingResult = ratingSum / ratingCount;
DataExtensions.SetValue(master,
"RatingSum"
, ratingSum);
DataExtensions.SetValue(master,
"RatingCount"
, ratingCount);
DataExtensions.SetValue(master,
"RatingResult"
, ratingResult);
blgManager.Publish(master);
blgManager.SaveChanges();
blgManager.Provider.SuppressSecurityChecks = securityChecksSettings;
}
public
void
AddBlogFieldsValue(Guid blogPostId,
decimal
ratingValue)
{
BlogsManager blgManager = BlogsManager.GetManager();
bool
securityChecksSettings = blgManager.Provider.SuppressSecurityChecks;
blgManager.Provider.SuppressSecurityChecks =
true
;
var post1 = blgManager.GetBlogPost(blogPostId);
var master = blgManager.GetBlogPost(post1.OriginalContentId);
decimal
ratingSum =
decimal
.Parse(DataExtensions.GetValue<
string
>(master,
"RatingSum"
));
int
ratingCount =
int
.Parse(DataExtensions.GetValue<
string
>(master,
"RatingCount"
));
ratingCount++;
ratingSum += ratingValue;
decimal
ratingResult = ratingSum / ratingCount;
DataExtensions.SetValue(master,
"RatingSum"
, ratingSum);
DataExtensions.SetValue(master,
"RatingCount"
, ratingCount);
DataExtensions.SetValue(master,
"RatingResult"
, ratingResult);
blgManager.Publish(master);
blgManager.SaveChanges();
blgManager.Provider.SuppressSecurityChecks = securityChecksSettings;
}
}
}
Before we create the template, we will host the service inside Sitefinity. Just go to SitefinityWebApp >> Sitefinity and create a new folder that will hold your service. Now copy one of the other .svc files inside the Sitefinity folder and change its Service property to Namespace.Class of your service. Here's what the code should look like:
<%@ ServiceHost Language="C#" Debug="false" Service="RatingControl.Service.RatingService" Factory="Telerik.Sitefinity.Web.Services.WcfHostFactory" %>
4) The final part here is creating a template. We will not need code-behind this is why we won't map external templates but just create one from inside Sitefinity. Just go to Design >> Widget Templates >> Create new Template and apply it to Blogs List (this can very easily be converted either to a Detail item template or to another List (like News List)). Here's the code for the template:
What I have done here is use jquery.ajax to consume the service that I created. This again is a reusable, trivial code, however the URL here is hardcoded, so don't forget to change it for your project, so that it can work.
Another thing that earlier said I would explain is not making use of the returned object. You can do easily do that yourself, so that no postback is required for the ItemRating to be updated, but for the example I decided to show you how to bind the value property of the Rating Result control to the custom fields of the Blog Post. So, if you want to change that behaviour, you can always set the Value of the RadRating inside this function:
function
ServiceSucceeded(result) {
// Here you can add code that makes use of the returned object
}
All properties of the returned object that we earlier created can be accessed through result.RatingSum (for example)
<%@ Control Language="C#" %>
<%@ Register TagPrefix="sf" Namespace="Telerik.Sitefinity.Web.UI.ContentUI" Assembly="Telerik.Sitefinity" %>
<%@ Register TagPrefix="sf" Namespace="Telerik.Sitefinity.Web.UI.Comments" Assembly="Telerik.Sitefinity" %>
<%@ Register TagPrefix="sf" Namespace="Telerik.Sitefinity.Web.UI" Assembly="Telerik.Sitefinity" %>
<%@ Register TagPrefix="sfPanel" Namespace="SitefinityWebApp.Controls" Assembly="SitefinityWebApp" %>
<%@ Register TagPrefix="sf" Namespace="Telerik.Sitefinity.Web.UI.PublicControls.BrowseAndEdit"
Assembly="Telerik.Sitefinity" %>
<%@ Register TagPrefix="telerik" Namespace="Telerik.Web.UI" Assembly="Telerik.Web.UI" %>
<%@ Import Namespace="Telerik.Sitefinity" %>
<%@ Import Namespace="Telerik.Sitefinity.Model" %>
<%@ Import Namespace="Telerik.Sitefinity.Blogs.Model" %>
<%@ Import Namespace="Telerik.Sitefinity.Modules.Blogs" %>
<
style
type
=
"text/css"
>
.label
{
visibility: hidden;
}
</
style
>
<
script
type
=
"text/javascript"
>
var Type;
var Url;
var Data;
var ContentType;
var DataType;
var ProcessData;
var method;
function CallService() {
$.ajax({
type: Type,
url: Url,
data: Data,
contentType: ContentType,
dataType: DataType,
processdata: ProcessData,
success: function(msg) {
ServiceSucceeded(msg);
},
error: ServiceFailed// When Service call fails
});
}
function ServiceSucceeded(result) {
// Here you can add code that makes use of the returned object
}
function ServiceFailed(result) {
alert('Service call failed: ' + result.status + '' + result.statusText);
Type = null;
Url = null;
Data = null;
ContentType = null;
DataType = null;
ProcessData = null;
}
function CallRequestRatingService(itemId, userRating) {
Type = "Post";
Url = "http://localhost:42093/Sitefinity/Services/Rating/RatingService.svc/RequestRating";
var msg2 = {"itemID": itemId, "userRating": userRating};
Data = JSON.stringify(msg2);
ContentType = "application/json; charset=utf-8";
DataType = "json";
Processdata = true;
method = "RequestRating";
CallService();
}
function OnClientRated(obj, args) {
var currentValue = obj.get_value();
var ratingID = obj.get_id()
var parsedID = '#' + ratingID;
var label = $(parsedID).siblings('.label');
var id = label.text();
CallRequestRatingService(id, currentValue);
}
</
script
>
<
telerik:RadListView
ID
=
"Repeater"
ItemPlaceholderID
=
"ItemsContainer"
runat
=
"server"
EnableEmbeddedSkins
=
"false"
EnableEmbeddedBaseStylesheet
=
"false"
>
<
LayoutTemplate
>
<
sf:ContentBrowseAndEditToolbar
ID
=
"MainBrowseAndEditToolbar"
runat
=
"server"
Mode
=
"Add"
>
</
sf:ContentBrowseAndEditToolbar
>
<
ul
class
=
"sfpostsList sfpostListTitleDateContent"
>
<
asp:PlaceHolder
ID
=
"ItemsContainer"
runat
=
"server"
/>
</
ul
>
</
LayoutTemplate
>
<
ItemTemplate
>
<
li
class
=
"sfpostListItem"
>
<
h2
class
=
"sfpostTitle"
>
<
sf:DetailsViewHyperLink
ID
=
"DetailsViewHyperLink1"
TextDataField
=
"Title"
ToolTipDataField
=
"Description"
runat
=
"server"
/>
</
h2
>
<
div
class
=
"sfpostAuthorAndDate"
>
<
asp:Literal
ID
=
"Literal2"
Text="<%$ Resources:Labels, By %>" runat="server" />
<
sf:PersonProfileView
ID
=
"PersonProfileView1"
runat
=
"server"
/>
<
sf:FieldListView
ID
=
"PostDate"
runat
=
"server"
Format
=
" | {PublicationDate.ToLocal():MMM dd, yyyy}"
/>
</
div
>
<
sf:FieldListView
ID
=
"PostContent"
runat
=
"server"
Text
=
"{0}"
Properties
=
"Content"
WrapperTagName
=
"div"
WrapperTagCssClass
=
"sfpostContent"
/>
<
sf:CommentsBox
ID
=
"itemCommentsLink"
runat
=
"server"
CssClass
=
"sfpostCommentsCount"
/>
<
br
/>
<
asp:Label
runat
=
"server"
Text
=
"Item rating"
></
asp:Label
>
<
telerik:RadRating
CssClass
=
"rating"
ID
=
"RadRating1"
runat
=
"server"
ReadOnly
=
"true"
ItemCount
=
"7"
Skin
=
"Sitefinity"
Orientation
=
"Horizontal"
Value='<%# decimal.Parse(((BlogPost)Container.DataItem).GetValue<string>("RatingResult")) %>' >
</
telerik:RadRating
>
<
asp:Label
runat
=
"server"
Text
=
"Your rating"
></
asp:Label
>
<
telerik:RadRating
ID
=
"RadRating3"
runat
=
"server"
Precision
=
"Exact"
SelectionMode
=
"Continuous"
ItemCount
=
"7"
OnClientRated
=
"OnClientRated"
Skin
=
"Sitefinity"
>
</
telerik:RadRating
>
<
asp:Label
CssClass
=
"label"
runat
=
"server"
Text='<%# Eval("Id")%>'></
asp:Label
>
<
sf:ContentBrowseAndEditToolbar
ID
=
"BrowseAndEditToolbar"
runat
=
"server"
Mode
=
"Edit,Delete,Unpublish"
>
</
sf:ContentBrowseAndEditToolbar
>
</
li
>
</
ItemTemplate
>
</
telerik:RadListView
>
<
sf:Pager
ID
=
"pager"
runat
=
"server"
>
</
sf:Pager
>
Now you can create a page, place a BlogLists control on it and select the template that you created and the RadRating will start working.
I hope this would be useful for you guys, if you have any questions, please feel free to ask.