+1-888-365-2779
Try Now
More in this section
Categories
Bloggers
Blogs RSS feed

Blog rating control with RadRating and the power of WCF Restful Services

by Svetoslav Petsov

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 { getset; }
 
        [DataMember]
        public decimal RateSum { getset; }
 
        [DataMember]
        public decimal UserRate { getset; }
 
        [DataMember]
        public Guid ItemId { getset; }
 
        [DataMember]
        public Guid UserID { getset; }
    }
}

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;
This makes sure that all registered users are able to vote. If this property is not set to true, only users with Create/Edit permissions on BlogPosts will be able to vote. Beforehand, I am also performing a check if the user is registered or not. If you wish to let anonymous users vote, you could easily remove that check.

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" %>
We are now ready to consume the service (at this point you will have to build Sitefinity.WebApp) Also, don't forget to add a reference to the project that contains the Model and the service.
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.

2 comments

Leave a comment
  1. Mat Feb 08, 2012
    Hi,
    thanks for your article!
    Because i can't build the Sitefinity.WebApp project: is it possible not to host the service inside Sitefinity, but "external"?
    What do i have to do to achieve this?
    Thanks
    Mat
  2. Boyko Feb 27, 2014
    Nice job!

    Leave a comment