jQuery Star Rating with ASP.NET MVC

There are a number of jQuery star rating plugins to choose from. All have their pros and cons. For this site, I decided to implement the one from FyneWorks because it allows for split stars. Here's how I did it.

The star rating system allows people to easily provide feedback on a blog item if they don't feel like submitting a comment. The rating plugin that I chose can be configured to allow any score to be applied, although I opted to allow people the provide a score from 1 - 5. I also wanted to give accurate information on the current average rating for each item, which meant that I need to show stars partially completed when an item's average rate is not a whole number. I also needed to prevent people rating an item more than once.

To begin with, I downloaded the plugin files, which consists of a number of .js files including jquery.MetaData.js, jquery.rating.pack.js and jquery.rating.js. There is also some documentation in the form of an html file, plus a style sheet for the rater and an image file for the stars. First thing I did was check the documentation for details on database integration, and was met with a message that this does not exist. The whole deal is completely up to me. Great. Then again, I suppose that there are so many options for connecting to databases - php to MySQL, ASP.NET to SQL Server via Linq To SQL, ADO.NET, Entity Framework, nHibernate, ColdFusion to whatever - that it would be a bit much to expect the authors to cover all angles. My method will be connecting to SQL Server via the Entity Framework, in keeping with previous articles on developing this site.

The rater will appear on the articles page. At the moment, there is nothing in the database to cope with rating articles, so I add two columns to the Articles table - Rating (int) and TotalRaters (int). As each rating comes in, I will increment the Rating value for the article by the score given by the rater. I will also add 1 to the TotalRaters for that article. I can calculate the average rating per article by dividing the Rating by the TotalRaters. I set the default value for both columns at 0. Having done that, I refresh my Model. The Article class is enhanced with the two extra properties:

I will be using a PartialView for the rater, so I create a class for the strongly typed ViewModel that it will use. The class will only contain 4 properties:


namespace MikesDotnetting.Models
{
  public class ArticleRating
  {
    public int ArticleID { get; set; }
    public int Rating { get; set;}
    public int TotalRaters { get; set; }
    public double AverageRating { get; set;}
  }
}

You may notice that this class is a subset of the full Article class properties (apart from the AverageRating property). You might be wondering why I am not using the full Article class. The reason is that I only need the data in this new class for the PartialView, and don't want to encumber it with more than necessary. Not only that, but exposing just these properties makes the PartialView more reusable.

Speaking of the PartialView, here it is:


<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<ArticleRating>" %>
<script type="text/javascript">
  $(function() {
    $('.star').rating('readOnly', true);
    $('#rater').hide();
    $('#rated').mouseover(function() {
      $('#rated').hide();
      $('#rater').show();
    });

    $('.auto-submit-star').rating({
      callback: function(value, link) {
        $.ajax({
          type: "POST",
          url: "/Article/Rate",
          data: $("#rate").serialize(),
          dataType: "text/plain",
          success: function(response) {
            if (response != 'false') {
              var data = eval('(' + response + ')');
              alert('Your rating has been recorded');
              $('#currentlyrated').html('Currently rated ' + data.AverageRating.toFixed(2) +
                ' by ' + data.TotalRaters + ' people');
            } else {
              alert('You have already rated this article');
            }
            $('#rater').hide();
            $('#rated').show();
          },
          error: function(response) {
            alert('There was an error.');
          }
        });
      }
    });
  });
</script>
  <div id="rated">
  <div style="float:left">
    <form id="Form1" method="post" action="">
      <input class="star {split:4}" type="radio" value="1" name="rating" <%= Utils.Check(0,0.25,Model.AverageRating) %>/>
      <input class="star {split:4}" type="radio" value="2" name="rating" <%= Utils.Check(0.25,0.5,Model.AverageRating) %>/>
      <input class="star {split:4}" type="radio" value="3" name="rating" <%= Utils.Check(0.5,0.75,Model.AverageRating) %>/>
      <input class="star {split:4}" type="radio" value="4" name="rating" <%= Utils.Check(0.75,1,Model.AverageRating) %>/>
      <input class="star {split:4}" type="radio" value="5" name="rating" <%= Utils.Check(1,1.25,Model.AverageRating) %>/>
      <input class="star {split:4}" type="radio" value="6" name="rating" <%= Utils.Check(1.25,1.5,Model.AverageRating) %>/>
      <input class="star {split:4}" type="radio" value="7" name="rating" <%= Utils.Check(1.5,1.75,Model.AverageRating) %>/>
      <input class="star {split:4}" type="radio" value="8" name="rating" <%= Utils.Check(1.75,2,Model.AverageRating) %>/>
      <input class="star {split:4}" type="radio" value="9" name="rating" <%= Utils.Check(2,2.25,Model.AverageRating) %>/>
      <input class="star {split:4}" type="radio" value="10" name="rating" <%= Utils.Check(2.25,2.5,Model.AverageRating) %>/>
      <input class="star {split:4}" type="radio" value="11" name="rating" <%= Utils.Check(2.5,2.75,Model.AverageRating) %>/>
      <input class="star {split:4}" type="radio" value="12" name="rating" <%= Utils.Check(2.75,3,Model.AverageRating) %>/>
      <input class="star {split:4}" type="radio" value="13" name="rating" <%= Utils.Check(3,3.25,Model.AverageRating) %>/>
      <input class="star {split:4}" type="radio" value="14" name="rating" <%= Utils.Check(3.25,3.5,Model.AverageRating) %>/>
      <input class="star {split:4}" type="radio" value="15" name="rating" <%= Utils.Check(3.5,3.75,Model.AverageRating) %>/>
      <input class="star {split:4}" type="radio" value="16" name="rating" <%= Utils.Check(3.75,4,Model.AverageRating) %>/>
      <input class="star {split:4}" type="radio" value="17" name="rating" <%= Utils.Check(4,4.25,Model.AverageRating) %>/>
      <input class="star {split:4}" type="radio" value="18" name="rating" <%= Utils.Check(4.25,4.5,Model.AverageRating) %>/>
      <input class="star {split:4}" type="radio" value="19" name="rating" <%= Utils.Check(4.5,4.75,Model.AverageRating) %>/>
      <input class="star {split:4}" type="radio" value="20" name="rating" <%= Utils.Check(4.75,5,Model.AverageRating) %>/>
      </form>
    </div>
    <p id="currentlyrated" style="float:left;padding-left:20px;">
    <%= Model.AverageRating > 0 ? "Currently rated " + Model.AverageRating.ToString("f") + " by " + Model.TotalRaters + " people" 
       : "<span style=\"color:red\">Not yet rated.  Be the first to rate this article!</span>"%>
    </p>
  </div>
  <div style="clear:both"></div>
  
  <div id="rater">
    <div style="float:left;">
      <form id="rate" method="post" action="">
      <input class="auto-submit-star" type="radio" name="score" value="1"/>
      <input class="auto-submit-star" type="radio" name="score" value="2"/>
      <input class="auto-submit-star" type="radio" name="score" value="3"/>
      <input class="auto-submit-star" type="radio" name="score" value="4"/>
      <input class="auto-submit-star" type="radio" name="score" value="5"/>
      <input type="hidden" name="ArticleID" value="<%=Model.ArticleID %>" />
      </form>
    </div>
    <p style="float:left;padding-left:20px;">
      Rate Now!
    </p>
  </div>
  <div style="clear:both"></div>

Right - before you simply copy and paste this and hope it will run, there is a fair amount to explain. I'll start with the second of the two divs. This is the 5 star rater which users will ise to score articles. I've used the option with a class of auto-submit-star, which according to the documentation is the one that does not need a submit button. It does need a bit of AJAX, which I will get to shortly. As well as the 5 stars for rating, the radio buttons are housed in a form which also contains a hidden field representing the ArticleID of the current article.

The previous div is the one that contains the split stars and will be shown to users when they first load an article. It will feature the current rating for that item. There are 20 of these, which when split into 4 represents 5 whole stars. Each star is capable of displaying ¼, ½, ¾ or a full star. That's as accurate as I need for display purposes. You will notice the <%= Utils.Check(0,0.25,Model.AverageRating) %> which appears in each input tag. This refers to a utility method that returns a string: checked="checked" if the average rating falls between two values. Here is the code for the method. It's dead simple:


public static string Check(double lower, double upper, double toCheck)
{
  return toCheck > lower && toCheck <= upper ? " checked=\"checked\"" : null;
}

I tried a number of ways to set the input as checked from client script, but in the end gave up. My lack of javascript knowledge eventually defeated me. I need to spend a lot more time examining the jquery.rater.js file, along with the MetaData.js file, I suspect.

Both the jquery.rater.pack.js file and the jquery.MetaData.js file are linked to in the main View, which is why they are not referenced in the Partial. The first segment of jQuery code sets the current rater (the first div) to disabled so that the user cannot use it to score with. It also hides the "live" rater, but adds a mouseover event to the disabled one which reveals the live rater, while hiding the disabled one. The message alongside the rater will change from a summary of the current score to one that invites users to Rate Now!

The second part of the jQuery code manages the AJAX form submission. Both the values from the form (the rate and the ArticleID) are serialized and posted to the Rate action on the Article Controller:


[AcceptVerbs("post")]
public ActionResult Rate(FormCollection form)
{
  var rate = Convert.ToInt32(form["Score"]);
  var id = Convert.ToInt32(form["ArticleID"]);
  if (Request.Cookies["rating" + id] != null)
      return Content("false");
  Response.Cookies["rating" + id].Value = DateTime.Now.ToString();
  Response.Cookies["rating" + id].Expires = DateTime.Now.AddYears(1);
  ArticleRating ar = repository.IncrementArticleRating(rate, id);
  return Json(ar);
}

Having taken the posted form values, the Action checks to see if there is a cookie called rating with the id of the article as part of its name, which would indicate that the user has rated this article previously. If there is, it sends back "false", but does not record the submitted score. If not, it creates a cookie showing that this article has been rated, and sets the expiry date of the cookie for one year's time. It then calls a method in the repository:


public ArticleRating IncrementArticleRating(int rate, int id)
{
  var article = de.ArticleSet
            .Where(a => a.ArticleID == id)
            .First();
  article.Rating += rate;
  article.TotalRaters += 1;
  de.SaveChanges();
  var ar = new ArticleRating()
             {
               ArticleID = article.ArticleID,
               Rating = article.Rating,
               TotalRaters = article.TotalRaters,
               AverageRating = Convert.ToDouble(article.Rating)/Convert.ToDouble(article.TotalRaters)
             };
  return ar;
}

which increments the Rating value by the score submitted, and the TotalRaters by 1. The updated values are then sent back to the Rate action to be serialized as JSON before being returned to the jQuery's success callback (repeated here to save you scrolling back up the page):


success: function(response) {
  if (response != 'false') {
    var data = eval('(' + response + ')');
    alert('Your rating has been recorded');
    $('#currentlyrated').html('Currently rated ' + data.AverageRating.toFixed(2) + 
      ' by ' + data.TotalRaters + ' people');
  } else {
    alert('You have already rated this article');
  }
  $('#rater').hide();
  $('#rated').show();
},
error: function(response) {
  alert('There was an error.');
}
          

If the response is not "false", it will be JSON. This is parsed using eval(), and while an alert is shown to the user confirming successful logging of their score, the paragraph containing the average rate is updated, and the rater is shown again.

Improvements

There are some things that I would like to have included. The first is the ability to set the initial state of the split star rater via javascript so that I don't need the Check() helper method. That would also apply when an article has just been rated so that I can update the stars as well as the paragraph containing the revised average rating. However, I tried a number of ways to achieve this without success. Simply using jQuery to set the attribute on the relevant radio didn't work. It just stopped the starts being shown. Using FireFox to view the generated DOM didn't help much either. I really need to know more javascript than I do.

The other thing that I would like to have done is to apply a mouseout to the second div - the one that allows people to rate. Again, just applying a mouseout to the div with the id of rater had the desired effect, until the mouse moved from one star to the next. It seemed that instead of being applied to the div itself, the mouseout had been applied to each individual star (or generated div containing a star). The plugin code actually generates some DOM which consists of a span containing a series of divs and <a> elements which house the star images. I could not understand why the mouseout did not apply to the containing elements, and could not find a solution.

Date Posted:
Last Updated:
Posted by:
Total Views to date: 62745

13 Comments

- Jean

Thank you for your post, great input.
For one of the improvements you suggested:
$('#rated').mouseenter(function() {
$('#rated').hide();
$('#rater').show();
});
$('#rater').mouseleave(function() {
$('#rater').hide();
$('#rated').show();
});

Have a nice day,
Jean

- april

sorry, I clicked kick it button mistakenly, thought it was a part of output. the article rocks.thanks!

- hamid


Great Artice , please also provide the dowbloadable code with your articles

- emad

your articles are great i hope you provide a working code to be downloaded
thanks

- Iain

Would moving some of the rating generation code into a loop (jquery or asp) be a good idea?

- Dany

If user will clear all the cookies, then it will be available him to rate again.

- Adeel

i want to ask you that from where i download the style sheet . because i downloaded the jquery.rating.css but there is no class named "auto-submit-star" . please help me out. can u give me full source code file or only this css file that contain "auto-submit-star" class

- Mike

@Adeel

There is no css declaration for the .auto-submit-star option because I applied no styles to it. It is simply a convenient way to use jQuery selectors to create a wrapped set.

- Berra Bertsson

Nice article, one question though. After a rate is submitted, the stars aren't updated/repainted, only the text with the current average is replaces.

Any ideas on how to implement that?

- norman

thank's for the idea but it's simple..

- sivasankari

good

- mayuri

Great Article. Can you please provide me source code.

- Mike

@mayuri

All the code you need is included in the article.
Add your comment

If you have any comments to make about this article, please use this form to do so. Make sure that your comment relates specifically to the article above. More general comments can be posted through the form on the Contact page.

Please note, all comments are moderated, and I end up deleting quite a lot. The kind of things that will ensure your comment is deleted without ever seeing the light of day are as follows:

  • Requests to fix your code (post a question to forums.asp.net instead, please)
  • Gratuitous links to your own site or product
  • Anything abusive or libellous
  • Spam

I do not pass email addresses on to spammers, so a valid one will assist me in responding to you personally if required.

Recent Comments

hosein ey 2/25/2015 1:56 PM
In response to ASP.NET MVC 5 with EF 6 - Working With Files
tnx for this article do you think next version of entityframework support's the sql server and ?...

Saywer Ford 2/25/2015 5:15 AM
In response to Optimising ASP.NET Web Pages Sites - Bundling And Minification
Great Article. I did everything right and working fine. How about page specific js files. Lets I...

Saravanan 2/24/2015 10:54 AM
In response to Optimising ASP.NET Web Pages Sites - Bundling And Minification
Hi, Great article about the Bundling and minification. Regards the caching as you mentioned the...

Justin 2/24/2015 10:43 AM
In response to Scheduled Tasks In ASP.NET With Quartz.Net
I'm having the same problem as Ingmar. Everything is working fine locally in Visual Studio, but I it...

Logan Mudlo 2/23/2015 4:59 PM
In response to WebMatrix - A First Application
Is there a way to prevent the automatic close on a Database.Open() call?...

Mog0 2/23/2015 11:16 AM
In response to ASP.NET MVC 5 with EF 6 - Working With Files
Probably a silly question but why did you disable cascade delete and implement it yourself?...

Jose-Maria 2/19/2015 3:29 PM
In response to Migrating From Razor Web Pages To ASP.NET MVC 5 - Views and Controllers
Just great ! Many thanks Mike....

Satyabrata Mohapatra 2/19/2015 12:02 PM
In response to Migrating From Razor Web Pages To ASP.NET MVC 5 - Views and Controllers
This is great!!...

Ryan Helmoski 2/19/2015 10:24 AM
In response to Conversion of a datetime2 data type to a datetime data type resulted in an out-of-range value
Thank you!...

Harsha 2/19/2015 8:27 AM
In response to iTextSharp - Adding Text with Chunks, Phrases and Paragraphs
Hi, Can text area be created so that I can type text in the area in pdf document? We have Add text...