Star User Rating Control in Asp.Net Using Jquery

With a little CSS, a tiny amount of jQuery and a little ingenuity you can easily create a reusable rating control in asp.net using jquery.

The Basics

For the CSS I decided there was no reason to re-invent the wheel, so I used Komodo Media’s CSS Star Rating Part Duex for the styles, although I did rename them (only because I prefer short, simple class names).

Here’s the basic HTML structure of the rating system:

<ul class="rating">
 <li><a class="one" title="1 out of 5">1</a></li>
 <li><a class="two" title="2 out of 5">2</a></li>
 <li><a class="three current" title="3 out of 5">3</a></li>
 <li><a class="four" title="4 out of 5">4</a></li>
 <li><a class="five" title="5 out of 5">5</a></li>
 </ul>

And here are the styles:

 .rating, .rating a:hover, .rating a:focus, .rating .current {
 background: url(/images/star.gif) left -1000px repeat-x;
 }
 .rating {
 position: relative;
 width: 125px;
 height: 25px;
 list-style: none;
 margin: 0;
 padding: 0;
 background-position: left top;
 }
 .rating li {
 display: inline;
 }
 .rating a, .rating .current {
 position: absolute;
 top: 0;
 left: 0;
 text-indent: -1000px;
 height: 25px;
 line-height: 25px;
 outline: none;
 overflow: hidden;
 border: none;
 }
 .rating a:hover, .rating a:active, .rating a:focus {
 background-position: left bottom;
 }
 .rating a.one {
 width: 20%;
 z-index: 6;
 }
 .rating a.two {
 width: 40%;
 z-index: 5;
 }
 .rating a.three {
 width: 60%;
 z-index: 4;
 }
 .rating a.four {
 width: 80%;
 z-index: 3;
 }
 .rating a.five {
 width: 100%;
 z-index: 2;
 }
 .rating .current {
 z-index: 1;
 background-position: left center;
 }

And, here’s the star image:

If you add what we have here to a page and view it in the browser, you’ll see a nice rating system. The current class represents the selected rating. That will show the lighter star, then hovering over the stars will show the darker star, otherwise the star outline will appear.

This looks quite nice and works great. However, there’s no real practical application to it. Clicking on one of the stars does nothing. Even if it did there is currently no way to send it back to the server, say, to update a rating field stored in a database field.

The Server Control

There are several ways to go about making this function. I chose to use an Asp.Net RadioButtonList with five ListItems with the appropriate values. Here’s the code:

<asp:RadioButtonList ID="rblRating" CssClass="hidden" RepeatLayout="Flow">
<asp:ListItem Value="1">1</asp:ListItem>
<asp:ListItem Value="2">2</asp:ListItem>
<asp:ListItem Value="3">3</asp:ListItem>
<asp:ListItem Value="4">4</asp:ListItem>
<asp:ListItem Value="5">5</asp:ListItem>
</asp:RadioButtonList>

Again, pretty simple. I always create a .hidden class in my stylesheet where I set the display: none. You cannot use Visible=”False” because the control will not be accessible via JavaScript because it won’t actually render to the page.

The jQuery

Now, before we make this a control, here’s the basics of the jQuery. We need to add a click event to the <a> tags inside the rating to do two things. First, remove the .current class from all the other <a> tags and then add the .current class back to the selected <a>.

Here’s the code for that:

$(document).ready(function() {
$('ul.rating a').click(function() {
var a = $(this);
a.parent().parent().find('a').removeClass('current');
a.addClass('current');
});
});

Not only does the <a> tag need the .current class added to it, the correct radiobutton needs checked. In the same click function we’ll do that:

$(document).ready(function() {
$('ul.rating a').click(function() {
var a = $(this);
a.parent().parent().find('a').removeClass('current');
a.addClass('current');
$('#<%= rblRating.ClientID %>').find('input[type=radio]').attr('checked','');
$('#<%= rblRating.ClientID %>').find('input[type=radio][value=' + a.html() + ']').attr('checked', 'checked');
});
});

That’s all there is to it. Now view this in the browser, click on the rating and the appropriate radio button will select. Of course if you have the RadioButtonList hidden you won’t see it, but you can un-hide it just to see it in action.

Now we can set the selected value of the RadioButtonList through server code and we can get the selected value on a post back. Only problem is that when setting the value from code-behind, setting the current star value requires more work. We would either have to make the <a> tags HyperLink controls or create the entire star rating HTML from code-behind and then add it to the page.

Or…we could make a reusable CompositeControl that we could simply drop on a page and it would do all the work for us.

The StarRating User Control

First off, we need to create a class. I called my StarRating under the namespace TD.Controls. The reason for the namespace is that I will be referencing this control like we would a regular ascx user control on the page. I decided to inherit the System.Web.UI.Control. This gives us all the properties and methods available for a generic web control. Here’s what we have so far. Notice you’ll need to add the System.Web, System.Web.UI, System.Web.UI.WebControls, System.Web.UI.HtmlControls, System.Text, and System.ComponentModel namespaces.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Web.UI.WebControls;
using System.Web.UI;
using System.Text;
using System.Web.UI.HtmlControls;
using System.ComponentModel;
namespace TD.Controls
{
 public class StarRating : System.Web.UI.Control
 {
 }
}

Adding Control Properties

Now that we have the framework  for this control, let’s add some public properties to allow for some customization as well as a property to hold the actual rating value.

#region "Properties"

private bool m_WrapWithDiv = true;
 [DefaultValue(true)]
 [Category("Behavior")]
 [Description("Determines whether or not to wrap control in an outer div")]
 public bool WrapWithDiv
 {
 get { return m_WrapWithDiv; }
 set { m_WrapWithDiv = value; }
 }

private string m_DivCssClass;
 [DefaultValue("")]
 [Category("Appearance")]
 [Description("The CssClass of the wrapping div")]
 public string DivCssClass
 {
 get { return m_DivCssClass; }
 set { m_DivCssClass = value; }
 }

private string _ratingCssClass = "rating";
 [DefaultValue("rating")]
 [Category("Appearance")]
 [Description("The CssClass of the star rating UL")]
 public string RatingCssClass
 {
 get { return _ratingCssClass; }
 set { _ratingCssClass = value; }
 }

private string _oneCssClass = "one";
 [DefaultValue("one")]
 [Category("Appearance")]
 [Description("The CssClass of the HTML Anchor for Value 1")]
 public string oneCssClass
 {
 get { return _oneCssClass; }
 set { _oneCssClass = value; }
 }

private string _twoCssClass = "two";
 [DefaultValue("two")]
 [Category("Appearance")]
 [Description("The CssClass of the HTML Anchor for Value 2")]
 public string twoCssClass
 {
 get { return _twoCssClass; }
 set { _twoCssClass = value; }
 }

private string _threeCssClass = "three";
 [DefaultValue("three")]
 [Category("Appearance")]
 [Description("The CssClass of the HTML Anchor for Value 3")]
 public string threeCssClass
 {
 get { return _threeCssClass; }
 set { _threeCssClass = value; }
 }

private string _fourCssClass = "four";
 [DefaultValue("four")]
 [Category("Appearance")]
 [Description("The CssClass of the HTML Anchor for Value 4")]
 public string fourCssClass
 {
 get { return _fourCssClass; }
 set { _fourCssClass = value; }
 }

private string _fiveCssClass = "five";
 [DefaultValue("five")]
 [Category("Appearance")]
 [Description("The CssClass of the HTML Anchor for Value 5")]
 public string fiveCssClass
 {
 get { return _fiveCssClass; }
 set { _fiveCssClass = value; }
 }

private string _selectedCssClass = "current";
 [DefaultValue("current")]
 [Category("Appearance")]
 [Description("The CssClass of the current rating")]
 public string selectedCssClass
 {
 get { return _selectedCssClass; }
 set { _selectedCssClass = value; }
 }

private int _Rating = -1;
 [DefaultValue(-1)]
 [Category("Properties")]
 [Description("The Rating value")]
 public int Rating
 {
 get
 {
 this.EnsureChildControls();
 if (rbl.SelectedIndex > -1)
 {
 _Rating = int.Parse(rbl.SelectedValue);
 }
 return _Rating;
 }
 set {
 _Rating = value;
 }
 }

#endregion

I’ve set the DefaultValue of the Rating property to -1 mainly because I prefer to set int values to -1 rather than 0 for a default value. You may notice that the get method of the Rating property calls the base method EnsureChildControls, that ensures that the child controls (the RadioButtonList we’ll be creating later) exists. And if the RadioButtonList has a selected item it will return the SelectedValue. And since -1 is the default value the star rating (and associated RadioButtonList) will be in it’s initial state.

All of the other properties deal with the layout and styling of the control. The properties for setting the css classes for each item will just give you an option if you don’t like my class names. I won’t be offended if you change them.

Now that all the properties are set up we can start building the actual control. Since the main controller will be RadioButtonList we will declare that as out as a private variable. Then we need to override the System.Web.UI.Control method CreateChildControls(). In this method we will instantiate (just a fancy word for create) our RadioButtonList. Once the RadioButtonList is created we’ll add the ListItems for the values 1-50. I’ll also add an ID so we can access it via jQuery.

And, I really can’t stand that by default RadioButtonLists are rendered inside a table, so I’m changing the RepeatLayout to Flow…I don’t care for that much either, but it’s better than a table. Lastly, I’ve added a style attribute to hide the RadioButtonList (remember, if you set it to Visible=False you won’t be able to access it from JavaScript).

CreateChildControls method

Once the RadioButtonList is created and the items are added, the SelectedValue of the RadioButtonList needs set if the Rating is not -1. Finally, the RadioButtonList needs added to the Control Collection of the control.

protected override void CreateChildControls()
 {
 rbl = new RadioButtonList();
 rbl.ID = this.ClientID + "_rblRating";
 rbl.RepeatLayout = RepeatLayout.Flow;
 rbl.Items.Add(new ListItem("1"));
 rbl.Items.Add(new ListItem("2"));
 rbl.Items.Add(new ListItem("3"));
 rbl.Items.Add(new ListItem("4"));
 rbl.Items.Add(new ListItem("5"));
 rbl.Style.Add("display", "none");
if (Rating > -1)
 {
 rbl.SelectedValue = Rating.ToString();
 }
 this.Controls.Add(rbl);
 base.CreateChildControls();
 }

Render Method

Now that the RadioButtonList has been created we need to render our StarRating control to the page. We do this by overriding the base Render(HtmlTextWriter writer) method.

protected override void Render(System.Web.UI.HtmlTextWriter writer)
 {
 if (WrapWithDiv)
 {
 writer.WriteBeginTag("div");
 if (!String.IsNullOrEmpty(DivCssClass))
 {
 writer.WriteAttribute("class", DivCssClass);
 }
 writer.WriteAttribute("id", this.ClientID);
 writer.Write(HtmlTextWriter.TagRightChar);
 }
 rbl.RenderControl(writer);
 HtmlGenericControl ul = new HtmlGenericControl("ul");
 ul.ID = this.ClientID + "_ulRating";
 ul.Attributes.Add("class", RatingCssClass);
 HtmlGenericControl li = new HtmlGenericControl("li");
 HtmlAnchor a = new HtmlAnchor();
 a.Attributes.Add("class", (Rating == 1 ? oneCssClass + " " + selectedCssClass : oneCssClass));
 a.Title = "1 of 5";
 a.InnerText = "1";
 a.HRef = "javascript:;";
 li.Controls.Add(a);
 ul.Controls.Add(li);
 li = new HtmlGenericControl("li");
 a = new HtmlAnchor();
 a.Attributes.Add("class", (Rating == 2 ? twoCssClass + " " + selectedCssClass : twoCssClass));
 a.Title = "2 of 5";
 a.InnerText = "2";
 a.HRef = "javascript:;";
 li.Controls.Add(a);
 ul.Controls.Add(li);
 li = new HtmlGenericControl("li");
 a = new HtmlAnchor();
 a.Attributes.Add("class", (Rating == 3 ? threeCssClass + " " + selectedCssClass : threeCssClass));
 a.Title = "3 of 5";
 a.InnerText = "3";
 a.HRef = "javascript:;";
 li.Controls.Add(a);
 ul.Controls.Add(li);
 li = new HtmlGenericControl("li");
 a = new HtmlAnchor();
 a.Attributes.Add("class", (Rating == 4 ? fourCssClass + " " + selectedCssClass : fourCssClass));
 a.Title = "4 of 5";
 a.InnerText = "4";
 a.HRef = "javascript:;";
 li.Controls.Add(a);
 ul.Controls.Add(li);
 li = new HtmlGenericControl("li");
 a = new HtmlAnchor();
 a.Attributes.Add("class", (Rating == 5 ? fiveCssClass + " " + selectedCssClass : fiveCssClass));
 a.Title = "5 of 5";
 a.InnerText = "5";
 a.HRef = "javascript:;";
 li.Controls.Add(a);
 ul.Controls.Add(li);
 ul.RenderControl(writer);
 if (WrapWithDiv)
 {
 writer.WriteEndTag("div");
 }
 }

The first thing is to check whether WrapWithDiv is true and render the outer div accordingly. Then add the DivCssClass to the outer div if set. After that we can render our RadioButtonList. The rest of the method creates the HTML <ul> to hold the visual star rating.

Each time a new <li> is created it checks to see if that value equals the Rating property. If it does it will add the selectedCssClass property to keep everything in sync. We’re almost there! Oh, by the way, notice I’ve set the href of each <a> to “javascript:;”…the reason being that if you set it to # the link actually does something. By setting it to javascript:; it will do nothing.

Now the jQuery

One thing we have to account for when writing our javascript is that we need the ability to add multiple StarRating controls to a page. We need to make sure that each one will work independently. This is why I set the IDs of both the RadioButtonList and the HtmlGenericControl to hold the <ul>. I used the current controls ClientID as a prefix because the ClientID will be unique to each instance of the control whereas just using the ID would not be unique and not only cause the StarRating control not to function properly, it would also give multiple elements the same ID which is a no-no.

To add the jQuery I’ll be using the ClientScriptManager’s RegisterStartupScript method. I’ll add this to a PageLoad method. To do this I must first add a Load method to the control. I also added an Init event to call the EnsureChildControls(). Not completely sure if this is necessary, but it doesn’t hurt anything either.

To add these methods to add a Constructor for the control.

public StarRating()
 {
 this.Init += _Init;
 this.Load += _Load;
 }

Now here are the _Init & _Load methods.

public void _Init(object sender, EventArgs e)
 {
 this.EnsureChildControls();
 }
public void _Load(object sender, EventArgs e)
 {
 ClientScriptManager cs = Page.ClientScript;
 String csname1 = this.ID + "_starratingjs";
 Type cstype = this.GetType();
 if (!cs.IsStartupScriptRegistered(cstype, csname1))
 {
 StringBuilder sb = new StringBuilder();
 sb.AppendFormat("var {0}=$('#{1}');", this.ID, this.ClientID);
 sb.AppendFormat("{0}.find('ul.{1} a').click(", this.ID, RatingCssClass);
 sb.Append("function() {");
 sb.Append("var a=$(this);var r=a.html();");
 sb.AppendFormat("var rbCont=$('#{0}');", rbl.ClientID);
 sb.Append("rbCont.find('input[type=radio]').attr('checked','');");
 sb.Append("rbCont.find('input[type=radio][value=' + r + ']').attr('checked', 'checked');");
 sb.Append("a.parent().parent().find('a').removeClass('" + selectedCssClass + "');a.addClass('" + selectedCssClass + "');");
 sb.Append("});");
cs.RegisterStartupScript(cstype, csname1, sb.ToString(), true);
 }
 }

Again, since there could be multiple controls on the same page we need to give the startup script a unique key as well. Then we check to see if the script has been added or not. If not we add it.

There are other ways you could build the JavaScript, but I chose a StringBuilder. First thing is I create a unique variable to hold the current control. Then I add a click event to each of the <a> tags inside the <ul>. Inside that click event I set a variable to hold the <a> that was clicked an get the html inside the <a> which should just be the numbers 1-5. I reset all the radio buttons inside the  RadioButtonList to be un-checked.

Then I find the corresponding radio button with the value of the html of the <a> and set it’s checked attribute to checked. All that remains is to remove the selectedCssClass from all the other <a> tags and then add the class back to the one that was clicked. That’s pretty much it.

Adding the control to a page

To add our new control to a page you can reference it in the Page directives like you would a user control, except you wouldn’t provide the Src attribute, you would provide the TD.Controls as the NameSpace. You also don’t need to provide a TagName. All the controls within the provided namespace will be available with the same TagPrefix.

<%@ Register TagPrefix="td" Namespace="TD.Controls" %>

Then on the page where you want the rating,

<td:StarRating runat="server" ID="StarRating1" />

You could also reference the control in the web.config Pages section. This would make it available on every page without having to add the Page Directive.

<pages>
 <controls>
 <add namespace="TD.Controls" tagPrefix="td"/>
 </controls>
 </pages>

Oh, one last important thing. Make sure the styles are set up in your stylesheet and the star.gif image is in your images directory (or wherever your stylesheet says it is)!

View your page in the browser and you should see 5 empty stars. Hover over a star and see them magically appear. Click on one and it should stay selected.

Just to make sure it works with more than one control on a page, let’s add another and explicitly set the rating.

<td:StarRating runat="server" ID="StarRating2" Rating="3" />

Now if you view your page in the browser you should see two star ratings, 1 blank and one with 3.

One last thing, let’s add a Button and Literal control and see if it posts back.

The HTML

<td:StarRating runat="server" ID="StarRating1" />
 <td:StarRating runat="server" ID="StarRating2" Rating="3" />
 <br />
 <asp:Button ID="Button1" runat="server" Text="Click Me" OnClick="Button1_Click" />
 <br />
 <asp:Literal ID="Literal1" runat="server"></asp:Literal><br />
 <asp:Literal ID="Literal2" runat="server"></asp:Literal>

The Code-Behind

protected void Button1_Click(object sender, EventArgs e)
 {
 Literal1.Text = "Star Rating 1 Rating = " + StarRating1.Rating.ToString();
 Literal2.Text = "Star Rating 2 Rating = " + StarRating2.Rating.ToString();
 }

There you have it! You now have a completely reusable and customizable star rating control! Enjoy!

Oops, almost forgot. The control will require you are referencing jQuery and have a ScriptManager on your page (or Master Page).