C# Tips and Tricks >  Ajax Rating System >  Article


Ajax Rating System

Introduction

This article describes an ASP.NET web module along with some chunks of Javascripts and ...Ajax to create a Rating System. This can be added /customized for any table to add a rating/score module. I was quite amazed, when i first saw the capability of rating multiple items at amazon website without refreshing the page. That nice piece of functionality followed me like destiny, till i needed to implement it in a web application.

Well ! putting it in a live scenario needs much more than just javascript. I started with a bit of frenzied hacking. To understand how exactly it works, by scanning the javascript from online asynchronous rating websites. That was just the beginning, on the way i learned much more than earlier imagined.  Here is the result of the effort. 

The target was to create a cross browser rating/score system like amazon or better:

  1. Rating: User should be able to Rate a record (1-5)
  2. Accuracy: Number of votes and total score should be accurate and saved and should not be lost in calculations
  3. Real time: Rate/score should be displayed after rating instantly 
  4. Reusable: It should be easily plugged into any table for reusability
  5. Avoiding multiple ratings: Basic mechanism to avoid multiple ratings by the same user
  6. Best approach: Compare the pros and cons for both Amazon and Ajax approach
  7. Security: Few words

* By Amazon Way i meant it can simulate the rating system like the amazon website, it does not claims that this is the way amazon is doing there rating.

In Action

 To hold your interest, here is how it will look (for single record), once completed 

   Rating in Action

Assumptions

Those were the initial thoughts, but to make it a general reusable and extensible module, I made some assumptions:

  • The table which will have the records to be rated will have a Primary Key ID (integer and identity) and the it will be the first field in the table.
  • The Table will have atleast two Fields: RatedBy  & Score
  • For the current example to work you will need atleast one extra field Title

Rating table structure

   Rating Table

web.config structure

The web.config has four keys, one for the connection string and the other for the name of the fields for RatedBy (for the number of user who rated the record)  and Score (For total Score) and the table from which you want to apply the Rating system.

   Web.config

Required images ( included with the source)

 Final Scores

   Final Scores  Stars

Pretty self explainatory final score can be in decimals 1, 2.5, 2, 4.5 etc, though while rating the images should be only 1-5

Actions

These are the actions which make the whole module:

  • Data access
  • Create rolling mouseover rating images
  • Rating Amazon way, Ajax way
  • Saving asynchronously using IFrames like Amazon (needs refresh)
  • Saving asynchronously using AJAX  (no refresh)
  • Rating multiple items in the same page
  • Basic security, rating for an item can be done only once by the user

Data access class

There is a standard data access class, clsDataAccess.cs, which handles all the data related actions.

Code: I have kept here only the names of the functions, just to give you a glimpse of the data access methods.

using System;
using System.Configuration;
using System.Data;
using System.Data.SqlClient; 

namespace Rating
{
    public class clsDataAccess
    // Class defination
    {
        public clsDataAccess()
        { }
        SqlConnection mycon = new SqlConnection(
          ConfigurationSettings.AppSettings["ConnectionString"]);
        // Opens database connection with in SQL SERVER
        public bool openConnection()
        // Closes database connection with in SQL SERVER
        public void closeConnection()
        // Getdata from the table required(given in query).
        public SqlDataReader getData(string query)
        // Save data usually,inserts and updates the data in table.
        public void saveData(string query)
        // Save data usually,inserts and updates the data.
        public void saveNewData(string query)
        // Delete data in database depending on the tablename.
        public int DeleteData(string query)
        // Get data by paging using datagrid.
        public SqlDataAdapter getDataforUpdate(string query)
        // Get data by paging using datagrid.
        public DataSet getDatabyPaging(string query)
        // check a particular value to see the validity.
        public int getCheck(string query)
        // Get a value of limit from the database table.
        public string getValue(string query,int j)
        //Log in method 
        public SqlDataReader Login(string query)
        // dynamically get all table names
        public DataTable getTablenames()
        // For Table operations
        public int TableWrite(string query)
    }
}

Creating rating Images with mouse over:

   Stars to Rate

 For this we need set of five images for the stars and five for the comments as shown above and two javascript function DisplayStars and DisplayMsg to display those images on mouseover.

Code

function DisplayStars (id, rating)
{
     var starID = "stars." + id;
     starTwinkler[id] = 0;
     msgTwinkler[id] = 0;
     document.write("<map name='starmap" + id +"'>");
     var i = 0;
     for (i = 1; i < 6; i++) {
     document.write("<area shape=rect " + 
     "coords='" + starMap[i] + "' " +
     "onMouseOver=\"StarMouseOver('" + id + "'," + i + ");\" " +
     "onMouseOut=\"StarMouseOut('" + id + "');\" " +
     "onClick=\"SaveStars('" + id + "'," + i + ");" +
     "\" >");
 }
 document.write("</map>");
 document.write("<img vspace=2 title = 'Rate Picture' src='" 
 + starImages[rating] + "'");
 document.write(" border=0 usemap='#starmap" + id);
 document.write("' id='" + starID + "'>");
}
function DisplayMsg (id, rating)
{
     var msgID = "messages." + id;
if ( rating == undefined ) {
document.write("<img vspace=2 height=11 src='" + nullStarMessage + "'");
    }
     else {
 document.write("<img vspace=2 height=11 src='" + starMessages[rating] + "'"); 
     }
 document.write("' id='" + msgID + "'>");
}

Javascript Mouseover functions are so old that nothing needs to be explained.You will also need functions for MouseOut OnClick, Initalize images etc, included in the source. 


											

Rating:  Amazon Way  

   Rating:Single Items

Amazon uses IFrames So the whole process of rating actually happens in another page which resides in the current page through iframe. Rate.aspx is the page which will be called inside an IFrame to call the Rating function

<iframe src='Rate.aspx?id=" + myid + "' width='310' 
Height='28' frameborder='0' bgcolor='#F2EFE0'></iframe>
Code

myid is the primary key of the record which we will pass to the rate page for saving the score for that record. frameborder='0' will make the IFrame invisible and seemlessly integrated into the page.

Rate.aspx PageLoad Event

string p = "Select * from " + ConfigurationSettings.AppSettings["MyPhotos"]
 + " WHERE id = '" 
+ Convert.ToInt32(Request.QueryString["id"]) + "'";
    
   clsDataAccess myDAR = new clsDataAccess();
   myDAR.openConnection(); 
   double myRatedBy = Convert.ToInt32(myDAR.getValue(p,Convert.ToInt32
  (ConfigurationSettings.AppSettings["RatedByField"])));
   double myScore = Convert.ToDouble(myDAR.getValue(p,Convert.ToInt32
   (ConfigurationSettings.AppSettings["ScoreField"])));
    
   double myCRating = Convert.ToDouble(Request.QueryString["Rating"]);
   double myTotalRating = (myScore + myCRating)/(myRatedBy+1);
  
   myDAR.closeConnection();
   double ORating = 0.0;
   if (myRatedBy>0)
    ORating = myScore/myRatedBy;
   
   string myTotalRatingString = "";
   if ((ORating <1)&&(ORating>0))
    myTotalRatingString = ".5";
   else if (ORating ==1.0)
    myTotalRatingString = "1";
   else if ((ORating >1)&&(ORating<2))
    myTotalRatingString = "1.5";
   else if (ORating ==2.0)
    myTotalRatingString = "2";
   else if ((ORating >2)&&(ORating<3))
    myTotalRatingString = "2.5";
   else if (ORating ==3.0)
    myTotalRatingString = "3";
   else if ((ORating >3)&&(ORating<4))
    myTotalRatingString = "3.5";
   else if (ORating ==4.0)
    myTotalRatingString = "4";
   else if ((ORating >4)&&(ORating<5))
    myTotalRatingString = "4.5";
   else if (ORating ==5.0)
    myTotalRatingString = "5";
   else if (ORating ==0.0)
    myTotalRatingString = "0";
   lblRating.Text  = "<IMG src='images/stars" + myTotalRatingString + ".gif'>" ;

Rate.aspx will simply take the myid (primary key) of the record, find out the value of RatedBy Field and Score Field of the records and Display the current rating of the record.

Saving Rating - the tricky but simple part

 A simple java script function of the stars image onclick event. This function calls another page rated.aspx for saving the current score for the particular record and displaying the final score.

function SaveRating(id, ratingType, ratingValue){

 var submitURL = ''
 + 'rated.aspx?'
 + 'id=' + id
 + '&Rating=' + ratingValue ;

 isRatingsBarChanged = true;
 window.location.href = submitURL;
}

Once the user rated the record simply redirect the rate.aspx to rated.aspx which calculates and saves the rating to the table and displays the stars accordingly, simple isn't it.

  Saving Rating

Rating: Ajax Way

Creating asynchronous function through ajax needs an extra layer but it is worth it, We have to create an object of XMLHttpRequest for cross browsers. Following is the easiest implementation. if (window.ActiveXObject) checks whether the user is using IE.

var xmlHttp;
function createXMLHttpRequest() 
 {
    if (window.ActiveXObject) 
    {
        xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
    } 
    else if (window.XMLHttpRequest) 
    {
        xmlHttp = new XMLHttpRequest();
    }
}

This object basically replaces the Save Rating function of "Amazon way" to a more simpler method. It can call any class (which we will discuss in a bit) and get the result from that function asynchronously.

Here is the code for that (id is the PK of the record , rating value is the current rating by the user

function SaveRatingAjax(id,ratingValue)
{
 rating = ratingValue;
 var submitURL = ''
 + 'ajaxRating.aspx?'
 + 'id=' + id
 + '&Rating=' + ratingValue ;
 
 myid = id;
 isRatingsBarChanged = true;
  createXMLHttpRequest();
  xmlHttp.onreadystatechange = handleStateChange;
  xmlHttp.open("GET", submitURL, true);
  xmlHttp.send(null);
}

What it does

It calls ajaxRating.aspx with the parameters and wait asyncronously for the ajaxRating page to return the value. Once the value is returned from the page it calls the handleStateChange function described below.

function handleStateChange()

{

if(xmlHttp.readyState == 4)

{

if(xmlHttp.status == 200)

{

var myScore = document.getElementById("lblScore" + myid).innerHTML;

var myTotal = document.getElementById("lblTotal" + myid).innerHTML;

var myVotes = document.getElementById("lblVotes" + myid).innerHTML;

myScore = (parseInt(myScore,10) + parseInt(rating,10));

myTotal = eval(parseInt(myTotal,10) + parseInt(5,10));

myVotes = eval(parseInt(myVotes,10) + parseInt(1,10));

 

document.getElementById("ltlstars" + myid).innerHTML =

'<IMG src=images/' + rating + 'star.gif>';

document.getElementById("ltlMsg" + myid).innerHTML = '<IMG src=images/saved.gif>';

document.getElementById("mytrating" + myid).innerHTML = '<IMG src="images/stars'

+ xmlHttp.responseText + '.gif">';

document.getElementById("lblScore" + myid).innerHTML = myScore;

document.getElementById("lblTotal" + myid).innerHTML = myTotal;

document.getElementById("lblVotes" + myid).innerHTML = myVotes;

}

else

{

alert("Error in AJAX");

}

}

}

AjaxRating.aspx just takes the score and the id of the record saves it to the table and give back the final score to handleStateChange function which uses the value to update dynamically the values displayed on the page.

document.getElementById: this is the javascript code which actually does the whole magic for dynamic value change in the page.

Code for AjaxRating.aspx in the pageLoad

string p = "Select * from " + ConfigurationSettings.AppSettings["MyPhotos"] 
+ " WHERE id = '" 
+ Convert.ToInt32(Request.QueryString["id"]) + "'";
    
    clsDataAccess myDAR = new clsDataAccess();
    myDAR.openConnection(); 
   
    double myScore = Convert.ToDouble(myDAR.getValue(p,Convert.ToInt32(
    ConfigurationSettings.AppSettings["ScoreField"])));
    double myRatedBy = Convert.ToInt32(myDAR.getValue(p,Convert.ToInt32(
    ConfigurationSettings.AppSettings["RatedByField"])));
    
    double myCRating = Convert.ToDouble(Request.QueryString["Rating"]);
    double myTotalRating = (myScore + myCRating)/(myRatedBy+1);
   
    string myTotalRatingString = "";
    if ((myTotalRating <1)&&(myTotalRating>0))
     myTotalRatingString = ".5";
    else if (myTotalRating ==1.0)
     myTotalRatingString = "1";
    else if ((myTotalRating >1)&&(myTotalRating<2))
     myTotalRatingString = "1.5";
    else if (myTotalRating ==2.0)
     myTotalRatingString = "2";
    else if ((myTotalRating >2)&&(myTotalRating<3))
     myTotalRatingString = "2.5";
    else if (myTotalRating ==3.0)
     myTotalRatingString = "3";
    else if ((myTotalRating >3)&&(myTotalRating<4))
     myTotalRatingString = "3.5";
    else if (myTotalRating ==4.0)
     myTotalRatingString = "4";
    else if ((myTotalRating >4)&&(myTotalRating<5))
     myTotalRatingString = "4.5";
    else if (myTotalRating ==5.0)
     myTotalRatingString = "5";
    
    int RatedBy = Convert.ToInt32(myRatedBy)+1;
    int TRating = Convert.ToInt32(myScore)+Convert.ToInt32(
    Request.QueryString["rating"]);
    string q = "UPDATE " + ConfigurationSettings.AppSettings["MyPhotos"]
    + " SET Score = '" + 
TRating + " ' , RatedBy = '" + RatedBy + "' WHERE id = '" + Convert.ToInt32
   (Request.QueryString["id"]) + "'";
    
    clsDataAccess myDA = new clsDataAccess();
    myDA.openConnection(); 
    myDA.saveData(q);
    myDA.closeConnection(); 
    Response.Write(myTotalRatingString);

So all ajaxRating.aspx does is save the rating in the Score field and add a count to RatedBy field and then Response.Write current TotalRating for the record and rest of the stuffs is handled by the handleStateChange function

Rating Multiple Items

 Since we are using the Id of the record, everything becomes as simple as it can be.

 For "Amazon way" create iframe for each record with different ID and the rate.aspx page will treat each one of them as seperate records.

 For Ajax , Since we need to display the Values dynamically on the page we need to create <Div> elements for each Field Score and RatedBy as well as for displaystars and displaymessages function

something like this <div id = 'lblVotes" + myid + ></div> dynamically which is retrieved

 var myVotes = document.getElementById("lblVotes" + myid).innerHTML;

and updated

 document.getElementById("lblVotes" + myid).innerHTML = myVotes;

in the handleStateChange. Thats all it needs (believe me) to update the field dynamically

Avoiding multiple ratings:

 So far we have got Rating system working with Javascript and XMLHttpRequest object , Basic security which normally required is - User should not be able to rate an item more than once.

There are number of methods to restrict the user. Like Code Project does it by using the userid of the voter and may be saving it, here we have used Cookies , right now it expires with the session but you can have it expire for a week or month or year or as long as you want.

Though there is an obvious downside to this, if the user manually deletes the cookie the information is lost and he will be able to rate again. 

 Even more strong kind of security can be implemeted using the i.p. address and saving it in a table.

Code behind

if (Request.Cookies["RatedAmz" + myid] == null) 
      {
       lblRatingAmz = "<iframe src='Rate.aspx?id=" + myid + "' width='310' 
      Height='28' frameborder='0' bgcolor='#F2EFE0'></iframe>";
      }
      else
      {
       HttpCookie cookieAmz = Request.Cookies["RatedAmz" + myid];
      
       StringBuilder sb1 = new StringBuilder();
       string myAmzRated =  sb1.Append( @"<TABLE CELLPADDING=0 CELLSPACING=0>
      <TR><TD>&nbsp;&nbsp;</TD><TD>
<table width='300' border='0' cellspacing='0' cellpadding='1'>" )
        .Append( @"<tr><td align='left' width='64'  
        height='26' valign='middle'>" )
        .Append( @"" + lblRatingAjax + "</td>" )
        .Append( @"<td align='left' height='26' width='101'>
        <img src = images/" 
       + cookieAmz.Values[0] + "star.gif></td>" )
        .Append( @"<td align='left' width='66'>" )
        .Append( @"<img src = images/saved.gif></td></tr>
       </table></TD></TR></TABLE>")
        .ToString();
       lblRatingAmz = myAmzRated;
      } 

For more curious users here is a way to see how this works, Just set the page directive trace="true"

  Trace equal to true 

You will see the session values in the page

  values in the cookies

We are done.  

Comparison: Pros and Cons for both approach

Pros and Cons discussed here are specifically for the rating module in hand, a lot of these limitation in both the ways can be eliminated by the use of versatile javascript.

Amazon approach

Pros- Simplicity

  • Simplicity in development, since everything is handled in a seperate page
  • Simplicity in extending for multiple items
  • Simplicity in maintaining
  • When you rate you can see the progressbar in the bottom active, so the user knows about the activity, there is also a downside to this see below

Cons-

  • It takes time to load, sometimes you can see a scrollbar before loading
  • You have to refresh the page to get the final score for the record
  • Needs multiple pages in the background for handling all
  • When you rate you can see the progressbar in the bottom active gives you a loaded feeling

Ajax approach

Pros-

  • Single page implementation, though you need atleast one class to be called
  • Dynamically change of values, makes it look neat
  • You can add (Powered by Ajax) in your website and feel happy about it :)

Cons-

  • A little difficult inital implementation , XMLHTTPRequest object seems tough though its not
  • Takes that extra development time to plan and create all the fields <div> controls
    which needs to be changed dynamically
  • When you use Ajax there is no way the user knows about the activity, so extra UI changes needs to be implemented (here the score and rated by fields are green)

A Word about Security

An article on browser based application wouldn't be complete without a word on security. Ajax or Javascript are no exception. The most important and vulnerable aspect of javascript is the transparency which it provides to the user. The javascript code can be seen in the html source of the page.

Lets go through a typical scenario, what a hacker or an eager student will do.

He will right click on the page and see the source, find this

<SCRIPT LANGUAGE="JavaScript" SRC="images/Rating.js"></SCRIPT>

furthur he will browse the link in internet explorer

.../rating/images/rating.js

 you see, a download window

   security screen one

He will download the javascript file for his "Operation"

And scanning through it he will find this piece of code

 rating = ratingValue;
 var submitURL = ''
 + 'ajaxRating.aspx?'
 + 'id=' + id
 + '&Rating=' + ratingValue ;

Bingo , Now he has some idea, how it works. What he will do is, browse the page directly and play with the parameters, something like

ajaxRating.aspx?id=1&Rating=100 

If developer is not careful enough, security breached.

The careful Workaround 

Add bounds on the javascript functions

if ((rating==1)||(rating==2)||(rating==3)||(rating==4)||(rating==5))
   {
 savedRatings[id] = rating;
 changedRatings[id] = 1;
 SaveRatingAjax(id, 'onetofive', rating);
 SwapStarMsg(id, 6);
   }
 else
   {
 alert("Rating Value out of the bound, Values can only be 1/2/3/4/5. 
     Current rating value: " + rating);
   }

Add server side validation to ensure integrity. i have added a simple check routine.

if ((Convert.ToInt32(Request.QueryString["Rating"])==1)||
   (Convert.ToInt32(Request.QueryString["Rating"])==2)||
   (Convert.ToInt32(Request.QueryString["Rating"])==3)||
   (Convert.ToInt32(Request.QueryString["Rating"])==4)||
   (Convert.ToInt32(Request.QueryString["Rating"])==5))
myTotalRating = (myScore + myCRating)/(myRatedBy+1); else myTotalRating = (myScore)/(myRatedBy);

Don't let the user directly call the page behind (here ajaxRating.aspx), which is exposed by the javascript.

server side validation for rated.aspx

if ((Convert.ToInt32(Request.QueryString["Rating"])==1)||
   (Convert.ToInt32(Request.QueryString["Rating"])==2)||
   (Convert.ToInt32(Request.QueryString["Rating"])==3)||
   (Convert.ToInt32(Request.QueryString["Rating"])==4)||
   (Convert.ToInt32(Request.QueryString["Rating"])==5))
{ Label1.Text = "<IMG src='images/" + Request.QueryString["rating"] + "star.gif'>" ; Label2.Text = "<IMG src='images/saved.gif'>" ; } else { Response.Redirect("Rate.aspx?id=" + Convert.ToInt32 (Request.QueryString["id"])); }

Other methods for security also includes obfuscating the javascript, making it difficult to understand. But the most important one is ofcourse the server side security and validation

Rating System in Action: Multiple records

   Rating system in Action

Article History

  • May 25, 2006: First published.
  • May 26, 2006: Added Pros and Cons for both approach
  • May 30, 2006: Added security as suggested by leppie and HyperX  
  • June 05, 2006: Added details on cookies and sessions to avoid multiple ratings 
  • Dec 15, 2006: Updated and Fixed the Online Demo
  • Jan 17, 2007: Updated Demo

And thanks

For coming so far! I hope you find this useful, and give me a vote/comment if you do and take care. If you liked this one , you might also wanna try my first article on Universal DBA here ^ , makes CRUD of tables a breeze.

Comments / Suggestions


screen  Add a Comment 
Subject  User  Date 
Last Visit: 5:56:39 PM, Tuesday, January 06, 2009


You can also reach me at: here

Rate This Page