Developing a star rating in ASP.NET MVC

Sheonarayan
Posted by in ASP.NET MVC category on for Advance level | Points: 250 | Views : 82962 red flag
Rating: 4.82 out of 5  
 11 vote(s)

In this article, we are going to learn how to develop a star rating system in ASP.NET MVC.
Recommendation
Read Auto populate DataTable columns to the Views in ASP.NET MVC before this article.

Introduction


Members rating plays a vital role in deciding whether a product or services should be bought or not. In this article, we are going to learn how to develop a star rating system in ASP.NET MVC.

NOTE: Word "Vote" and "Rating" are used interchangeably in this article. So do not get confused, both convey the same meaning.

Database structure


There are two small changes needed in our database for this rating system
  1. Add a field called "Votes" (or "Rating" whatever best suits you) in the existing database table where we are going to save other data of the post.



  2. Add another database table called "VoteLog" that will save each vote details. Below are details of each field of this table



    • AutoId - auto increment
    • SectionId - id of different sections of the website (we might be interested in implementing this rating system in multiple sections of the website, so all rating details will be saved into this database table)
    • VoteForId - unique id of the record for which member has rated
    • UserName - username of the member who has rated
    • Vote - rate given by a member for this post
    • Active -whether this record is active and should be counted

Different status of Rating control


Status 1 - When a visitor is visiting the website



Status 2 - When a visitor has logged in (is a member of the website)



Status 3 - After rating the post



Status 4 - When a visitor had rated the post and coming back to the same post again and browser cookie is still present in visitors' machine.


Status 5 - When a visitor had voted and browser cookies are cleared and he/she tried to vote again for the same post.




Lets start developing Star rating system 


Step 1


Create a partial view named _VoteNow.cshtml in the Views/Shared folder and copy-paste below code.
@model string
@{
    var url = Request.Url.AbsolutePath;    
}
@if (!User.Identity.IsAuthenticated)
{
    <text>Please <a href="/Account/Login?ReturnUrl=@Request.Url.AbsolutePath" title="Login to rate">Login</a> to rate</text>
    return;
}
@if (Request.Cookies[url] == null) {
    <div id="ratingDiv" class="smallText"> Poor 
        <img src="/images/whitestar.gif" alt="" class="ratingStar" data-value="1" /><img src="/images/whitestar.gif" alt="" class="ratingStar" data-value="2" /><img src="/images/whitestar.gif" alt="" class="ratingStar" data-value="3" /><img src="/images/whitestar.gif" alt="" class="ratingStar" data-value="4" /><img src="/images/whitestar.gif" alt="" class="ratingStar" data-value="5" /> Excellent
        <label id="lblResult"></label>
    </div>
    <style type="text/css">
        .ratingStar { 
            cursor:pointer;
        }
    </style>
    <script type="text/javascript">
        var clickedFlag = false;
        $(".ratingStar").mouseover(function () {
            $(this).attr("src", "/images/yellowstar.gif").prevAll("img.ratingStar").attr("src", "/images/yellowstar.gif");
        });
        $(".ratingStar, #radingDiv").mouseout(function () {
            $(this).attr("src", "/images/whitestar.gif");
        });
        $("#ratingDiv").mouseout(function () {
            if (!clickedFlag)
            {
                $(".ratingStar").attr("src", "/images/whitestar.gif");
            }
        });
        $(".ratingStar").click(function () {
            clickedFlag = true;
            $(".ratingStar").unbind("mouseout mouseover click").css("cursor", "default");

            var url = "/Home/SendRating?r=" + $(this).attr("data-value") + "&s=5&id=@Model&url=@url";
            $.post(url, null, function (data) {
                $("#lblResult").html(data);
            });
        });
        $("#lblResult").ajaxStart(function () {
            $("#lblResult").html("Processing ....");
        });
        $("#lblResult").ajaxError(function () {
            $("#lblResult").html("<br />Error occured.");
        });
    </script>
}else{
    <text><span style="color:green;">Thanks for your vote !</span></text>
}
In the above code snippet, we are doing following
  1. Getting current browser url ApplicationPath (path without domain name) that will be used to send to the server to create unique cookie for this post

  2. Checking whether User is authenticated, if not showing the Login link (refere to Status 1 above).

  3. Checking if user has already voted, If yes, go to else block and write Thanks message (refer to Status 4). If not, show the rating control (Status 2) that contains following
    • 5 white images 
    • a html lable element
    • a .ratingStar class
    • lines of JavaScript code
      • mouseover() - When a visitor mouse over white stars, the same gets replaced with yellow star  with the help of .ratingStar css class. Notice the .prevAll function of jQuery that is intelligent enough to find all previous elements with same .css class and change its src value.
      • mouseout() 
        • When user keeps his mouse out of image, we are keeping the white images back
        • If user has already rated and then keeping mouse out of image then we are NOT replacing yellow image to white. This is being checked with the help of clickedFlag variable.
      • click() When user click on the star images clickedFlag will be set to true and mouseover, mouseout, click events will be unbounded from the img element.

        jQuery.post method is used 
        • to send the data-value of the start rating image
        • section for which this post belongs to
        • the unique id of this post and 
        • the url (ApplicationPath) of this post.

          All above data is being sent to /Home/SendRating action under HomeController. You may change the name of controller and action method if that suits you better.

          Once successful, the data coming from server is being written to the label element.

      • .ajaxStart() When user clicks on the star images and ajax post event occurs, we are writing "Processing ..." status to notify user that his rating is being processed.
      • .ajaxError() When an error occurs, error message is written in the label. The error message can be can be changed.

Step 2


Create a SendRating action method in HomeController

public JsonResult SendRating(string r, string s, string id, string url)
        {
            int autoId = 0;
            Int16 thisVote = 0;
            Int16 sectionId = 0;
            Int16.TryParse(s, out sectionId);
            Int16.TryParse(r, out thisVote);
            int.TryParse(id, out autoId);
            
            if (!User.Identity.IsAuthenticated)
            {
                return Json("Not authenticated!");
            }

            if (autoId.Equals(0))
            {
                return Json("Sorry, record to vote doesn't exists");
            }

            switch (s)
            {
                case "5" : // school voting
                    // check if he has already voted
                    var isIt = db.VoteModels.Where(v => v.SectionId == sectionId &&
                        v.UserName.Equals(User.Identity.Name, StringComparison.CurrentCultureIgnoreCase) && v.VoteForId == autoId).FirstOrDefault();
                    if (isIt != null)
                    {
                        // keep the school voting flag to stop voting by this member
                        HttpCookie cookie = new HttpCookie(url, "true");
                        Response.Cookies.Add(cookie);
                        return Json("<br />You have already rated this post, thanks !");
                    }

                    var sch = db.SchoolModels.Where(sc => sc.AutoId == autoId).FirstOrDefault();
                    if (sch != null)
                    {
                        object obj = sch.Votes;

                        string updatedVotes = string.Empty;
                        string[] votes = null;
                        if (obj != null && obj.ToString().Length > 0)
                        {
                            string currentVotes = obj.ToString(); // votes pattern will be 0,0,0,0,0
                            votes = currentVotes.Split(',');
                            // if proper vote data is there in the database
                            if (votes.Length.Equals(5))
                            {
                                // get the current number of vote count of the selected vote, always say -1 than the current vote in the array 
                                int currentNumberOfVote = int.Parse(votes[thisVote - 1]);
                                // increase 1 for this vote
                                currentNumberOfVote++;
                                // set the updated value into the selected votes
                                votes[thisVote - 1] = currentNumberOfVote.ToString();
                            }
                            else
                            {
                                votes = new string[] { "0", "0", "0", "0", "0" };
                                votes[thisVote - 1] = "1";
                            }
                        }
                        else
                        {
                            votes = new string[] { "0", "0", "0", "0", "0" };
                            votes[thisVote - 1] = "1";
                        }

                        // concatenate all arrays now
                        foreach (string ss in votes)
                        {
                            updatedVotes += ss + ",";
                        }
                        updatedVotes = updatedVotes.Substring(0, updatedVotes.Length - 1);

                        db.Entry(sch).State = EntityState.Modified;
                        sch.Votes = updatedVotes;
                        db.SaveChanges();

                        VoteModel vm = new VoteModel()
                        {
                            Active = true,
                            SectionId = Int16.Parse(s),
                            UserName = User.Identity.Name,
                            Vote = thisVote,
                            VoteForId = autoId
                        };
                        
                        db.VoteModels.Add(vm);
                        
                        db.SaveChanges();

                        // keep the school voting flag to stop voting by this member
                        HttpCookie cookie = new HttpCookie(url, "true");
                        Response.Cookies.Add(cookie);
                    }
                    break;
                default:
                    break;
            }
            return Json("<br />You rated " + r + " star(s), thanks !");
        }

In the above code, we are doing following
  1. retrieving all the data (like rate given, section, unique post id and url) coming in to this method from the _VoteNow partial view.

  2. Doing some basic validation, liked if a user is logged in or not and whether unique post id (autoid) exists

  3. Using switch case based on the sectionId, If we do not have different sections in the website, we can write following code without switch case block.

  4. In the switch case block, we are doing following
    • Checking if this user has already voted, if yes we are returning already voted message after creating cookies based on the url so that next time user comes the rating control will not display but  Thanks message appears.
    • Getting the record for which this rate was given and retrieving the current Votes (Rates). The rates are being stored in this field as "0,0,0,0,0" format.
      • Where first value is the count of how many users rated 1
      • second value is the count of how many users rated 2
      • third value is the count of how many users rated 3 and so on
    • Based on what rate has been give, the corresponding value is increased by one. If not, probably, this post is being rated first time so default value is being set and corresponding rate count is increased to 1.

    • Again, we are looping through the array of current votes and updating into the Votes field back.

    • Instantiating the VoteModel (VoteLog table) and saving corresponding data into the database. In this scenario, this table is being used only to check if a particular user has already rated however this table can also be used to count how active a user is in voting or to give point or appreciation for a particular user etc.

    • We are creating a cookie in the browser based on the url so that next time when this user visits this page, he gets thanks message but not rating option.

    • At last we are returning a Json result of how much rate was given by this user and thanks !

Step 3


Displaying current rating.

Till now, we have developed enough infrastructure for user to rate a post. Now its time to display the ratings. Create _VoteShow.cshtml partial view in the Views/Shared folder and copy-paste below code.
@model string

@{
    Single m_Average = 0;

    Single m_totalNumberOfVotes = 0;
    Single m_totalVoteCount = 0;
    Single m_currentVotesCount = 0;
    Single m_inPercent = 0;
    var thisVote = string.Empty;
    
    if (Model.Length > 0)
    {
        // calculate total votes now
        string[] votes = Model.Split(',');
        for (int i = 0; i < votes.Length; i++)
        {
            m_currentVotesCount = int.Parse(votes[i]);
            m_totalNumberOfVotes = m_totalNumberOfVotes + m_currentVotesCount;
            m_totalVoteCount = m_totalVoteCount + (m_currentVotesCount * (i + 1));
        }

        m_Average = m_totalVoteCount / m_totalNumberOfVotes;
        m_inPercent = (m_Average * 100) / 5;

        thisVote = "<span style=\"display: block; width: 65px; height: 13px; background: url(/images/starRating.png) 0 0;\">" +
            "<span style=\"display: block; width: "+ m_inPercent + "%; height: 13px; background: url(/images/starRating.png) 0 -13px;\"></span> " +
            "</span>" +
            "<span class=\"smallText\">Overall ratings: <span itemprop=\"ratingCount\">" + m_totalNumberOfVotes + "</span> | Rating: <span itemprop=\"ratingValue\">" + m_Average.ToString("##.##") + "</span> out of 5 </span>  ";
    }
}
 <div itemprop="aggregateRating" itemscope itemtype="http://schema.org/AggregateRating">
    <meta itemprop="bestRating" content="5" />
    <meta itemprop="worstRating" content="1">
    <meta itemprop="ratingValue" content="@m_Average.ToString("##.##") %>" />
      @Html.Raw(thisVote)
  </div>

In the above code, we are doing following
  1. Checking if we are getting any ratings on this view from the this partial view is being called

  2. Splitting the rates given (that was in the form of "0,0,0,0,0") into an array and looping through to count the total number of rates, total of rates given and getting its average and percentage.

  3. Generating a html string with complete details of the rating given with all statistics and writing on the page.
The itemprop itemscope used in the div and other elements are microdata to let search engines know the rating system of this post.

Step 4


Calling rating controls (partial vies) on the post page.

<div class="tr" style="background-color:#f1f1f1;">
                <div class="td0">Please rate this school</div>
                <div class="td">@Html.Partial("_VoteNow", Model.AutoId.ToString())</div>
                <div class="td">@Html.Partial("_VoteShow", Model.Votes)</div>
            </div>
While calling _VoteNow, we are passing the unique post id as parameter and while calling _VoteShow, we are passing current votes (Rates) in the form of  "x,x,x,x,x" as parameter and here is the final output displays on the web page.


Hurrah!!! our own rating system is up and live! Have a look at it here.


Conclusion


In this article, we learnt how to create a star rating system in ASP.NET MVC. There are many plug-ins available however they may not suit specific needs of every project. Developing our own gives us more control over how and what we want to display.

Hope this article was useful, thanks for reading. Do refer DotNetFunda.com to your friends and colleagues and do not forget to write your feedback.
Page copy protected against web site content infringement by Copyscape

About the Author

Sheonarayan
Full Name: Sheo Narayan
Member Level: HonoraryPlatinum
Member Status: Administrator
Member Since: 7/8/2008 6:32:14 PM
Country: India
Regards, Sheo Narayan http://www.dotnetfunda.com

Ex-Microsoft MVP, Author, Writer, Mentor & architecting applications since year 2001. Connect me on http://www.facebook.com/sheo.narayan | https://twitter.com/sheonarayan | http://www.linkedin.com/in/sheonarayan

Login to vote for this post.

Comments or Responses

Posted by: Rohit421991 on: 6/8/2015 | Points: 25
How can I get related images from this article?
Posted by: Net4u on: 7/3/2015 | Points: 25
Hi,
I searching for days for this. I have some question about usage scenario:
First of all I am interested to use in two places: first on each record on list like view (e.g. Index) and second in a Detail view for the selected record (so both are bind to same model e.g. in my case Jobs and/or Candidates). I have in my project several ViewModels that consolidates several models in one place.
Second I have in mind to extend also with some extra text fields (e.g. Commments, VoteDate etc.)
In both situations I am confused because it seems not very evident for me to have in same View display data and add data
Posted by: Chrisc3 on: 6/10/2016 | Points: 25
The model item passed into the dictionary is of type 'System.Data.Entity.DynamicProxies.Item_73CB21825157346A83D6210971F04F91B65EA5C60D7440A30EB283BBC8168589', but this dictionary requires a model item of type 'System.String'.

The error goes on this line : <div class="td">@Html.Partial("_VoteShow", Model.Votes)</div>

If you could help me I would be grateful. Thank you for your post and your sharing!
Posted by: Chrisd0tn3t on: 7/12/2016 | Points: 25
If you want keep posting you should be more clear because this post is not organized well at all. First of all you need to tell the relationships in database between schools table and VoteLog and how Primary Key and Foreign Key goes. After that you certainly need to refer section table and how it works. These are the most needed even it is code first migration or database first. After these you should explain the back end of the post controller and views more clearly and I mean in the sectionId Votes of school table and the corresponding Vote of VoteLog table. Not well organized post-article at all. Thank's for sharing some of your code though. Also you should include star images because I had to search them on the other site you made and you link at this post.

Login to post response

Comment using Facebook(Author doesn't get notification)