Spaanjaars.Toolkit.ContentRating: Version 1.1

Ever since I wrote the initial version of my ContentRating control back in 2006, I received a massive amount of feedback, both as comments below the article and as private e-mails. Not surprisingly, if you consider the article has been read over 19,000 times and has been rated 444 times (at the time of writing).

Besides getting a lot of "thank you's" from people who liked the control, I also got a lot of requests for a real-world example of a test site using the control. The test site that shipped with the control used fake data stored in ViewState to simulate a real backing store which obviously didn't cut it for a lot of people.

Also, a reader called vgt pointed out a bug in the control where an existing cookie would be overwritten by a new one one, effectively allowing you to vote for the previous item again.

Finally, I had a few requests of my own: I didn't like the default data source of 5 integers if you didn't supply a data source yourself. I also didn't like it that the control didn't raise an exception when you tried to data bind it without a valid data source.

So, enough reasons to fire up Visual Studio and get my hands dirty on some control fixing.

V1.1 of the Spaanjaars.Toolkit.ContentRating Control

(Note, if you haven't read the original article or looked at the control, now is a great time to do so).

With .NET 3.5 being out for more than half a year, and with Visual Web Developer 2008 Express Edition being completely free for everyone, I decided to upgrade the control to .NET 3.5. Don't worry if you're still on .NET 2.0: the actual .cs files of the control can easily be used in a .NET 2.0 project; simply start a new project and drop the files in the solution.

The test web site is a different story as it uses LINQ to get and update data. However, the database schema and stored procedure can easily be reused in your own .NET 2.0 application and accessed with a bit of ADO.NET code.

So What's New?

The core functionality of the control hasn't changed. Everything that was there is still there. However, I changed the implementation of the internal data source a little. Previously, it defaulted to this:

[ContentRating.cs]
private int[] _dataSource = new int[] { 0, 0, 0, 0, 0 };

This way, the control would always have a valid data source, whether you set one yourself or not. Now, in the new implementation, _dataSource defaults to null:

[ContentRating.cs]
private int[] dataSource;

Additionally, the data source is checked in the DataBind method:

[ContentRating.cs]
public override void DataBind()
{
  if (ItemId == null)
  {
    throw new ArgumentException(@"Can't bind a datasource 
        without a valid ItemId");
  }

  if (_dataSource == null || _dataSource.Length == 0)
  {
    throw new InvalidOperationException(
	       @"Can't bind without a valid data source.");
  }
  
  base.DataBind();
  Controls.Clear();
  ClearChildViewState();
  TrackViewState();
  ViewState["RateItems"] = _dataSource;
  CreateControlHierarchy(_dataSource);
  ChildControlsCreated = true;
}

This way, you get a crystal clear error message when you forget to set the data source, rather than the control silently defaulting to an array of 5 integers.

The second change I made was in the way I set the cookie. Previously, I had this code:

[ContentRating.cs]
if (!myArgs.Cancel)
{
  // Create a new cookie value for this rate.
  rateCookie = new HttpCookie("Rate");
  rateCookie.Expires = DateTime.Now.AddMonths(6);
  rateCookie.Values.Add(cookieKey, cookieValue);
  Context.Response.Cookies.Add(rateCookie);
  // Raise the Rated event to notify clients.
  OnRated(myArgs);
  handled = true;
}

This code simply ignored the fact that the cookie might already contain one or more keys. The code has now been changed to this

[ContentRating.cs]
if (!myArgs.Cancel)
{
  // Create a new cookie value for this rate.
  rateCookie = Context.Request.Cookies[CookieKey]; // CookieKey is a new constant

  if (rateCookie == null)
  {
    rateCookie = new HttpCookie(CookieKey);
  }
  rateCookie.Expires = DateTime.Now.AddMonths(6);
  rateCookie.Values[cookieSubKey] = cookieValue;
  Context.Response.Cookies.Add(rateCookie);
  // Raise the Rated event to notify clients.
  OnRated(myArgs); 
  handled = true;
}

Now the code checks if there is an existing cookie. If there's not, a new cookie is created. Either way, the sub key is added using the Values collection of the cookie.

That's pretty much what has changed in the control itself. I have made some minor changes (mostly evolving around naming conventions), so check out the ChangeLog.txt file that comes with the solution if you want to see all the changes.

The Sample Site

To make it easier to test out the control, I supplied a simple web site with the initial control's release. However, the fact that it used a fake data store made it difficult for a lot of people to implement this control in a real world web site. So, to accommodate for these people, I have updated the sample web site and supplied a SQL Server 2005 database with a single table called Rating. The table looks like this:

The Design of the Rating Table
Figure 1 - The Rating Table

This table simply stores the ID of a content item, and has five columns to store the rated values. Each of these columns keeps track of the number of times a user has given a specific rating to an item.

I rely on LINQ to get the rating values for me; you'll see how this works in a bit. However, to update a rating, I am using the following stored procedure that I have mapped to a method in my LINQ to SQL Classes design surface. The stored procedure looks like this:

[sprocRatingUpdate]
CREATE  PROCEDURE [dbo].[sprocRatingUpdate]
  /*  '===============================================================
  '   NAME:                sprocRatingUpdate
  '   DATE CREATED:        2008-07-16
  '   CREATED BY:          Imar Spaanjaars
  '   CREATED FOR:         Imar.Spaanjaars.Com
  '===============================================================*/

  @contentId int,
  @rateId int

  AS
  
  IF NOT EXISTS (SELECT 1 FROM Rating WHERE ContentId = @contentId)
  BEGIN
    INSERT Rating (ContentId) VALUES(@contentId)
  END
  
  IF (@rateId = 1)
  BEGIN
    UPDATE Rating SET Rating1 = Rating1 + 1 WHERE ContentId = @contentId
  END
  IF (@rateId = 2)
  BEGIN
    UPDATE Rating SET Rating2 = Rating2 + 1 WHERE ContentId = @contentId
  END
  IF (@rateId = 3)
  BEGIN
    UPDATE Rating SET Rating3 = Rating3 + 1 WHERE ContentId = @contentId
  END
  IF (@rateId = 4)
  BEGIN
    UPDATE Rating SET Rating4 = Rating4 + 1 WHERE ContentId = @contentId
  END
  IF (@rateId = 5)
  BEGIN
    UPDATE Rating SET Rating5 = Rating5 + 1 WHERE ContentId = @contentId
  END

In order to avoid Dynamic SQL, I have an UPDATE statement for each possible rate value, ranging from 1 to 5. I am sure there are cleaner ways to do this, but this one has always worked for me.

So, what happens when you pass a content ID and a rate value to this stored procedure? First, it tries to find an existing record for the requested ContentId. If the item doesn't exist, it is inserted. The remainder of the code then updates the (new or existing) item and increases the selected rate value by 1.

How is this procedure called in the sample web site you may wonder? Because of the LINQ to SQL diagram, there isn't much implementation. Getting a selected rating is as easy as firing a simple LINQ query:

[Default.aspx.cs]
using (RatingDataContext db = new RatingDataContext())
{
  int[] rateValues;

  // Get the item through LINQ. SingleOrDefault returns NULL when the item is not found.
  Rating result = (from ra in db.Ratings
                  where ra.ContentId == contentId
                  select ra).SingleOrDefault();

  if (result == null)
  {
    // Initialize to zero's
    rateValues = new int[] { 0, 0, 0, 0, 0 };
  }
  else
  {
    // Get the values from the result Rating instance.
    rateValues = new int[] { result.Rating1, result.Rating2, 
	              result.Rating3, result.Rating4, result.Rating5 };
  }

  ContentRating1.ItemId = ContentId;
  ContentRating1.DataSource = rateValues;
  ContentRating1.DataBind();
  lblContentId.Text = ContentId.ToString();
}

Notice how SingleOrDefault is called to get an instance of Rating or a a null value out of the LINQ query. This is useful if the item has never been rated before and thus has no associated record in the database.

If SingleOrDefault returned a value, it's used to initialize an array of integers based of the values on the result instance. Otherwise, an empty array of 5 integers is created.

Updating the Rating table is even easier. I mapped the stored procedure sprocRatingUpdate to a UpdateRating method in my model, so all I need to do now is call this method and pass two values, then call BindData to refresh the rating control:

[Default.aspx.cs]
protected void ContentRating1_Rated(object sender, Spaanjaars.Toolkit.RateEventArgs e)
{
  using (RatingDataContext db = new RatingDataContext())
  {
    // Execute the stored procedure that updates the rating
    db.UpdateRating(ContentId, e.RateValue);
    BindData(ContentId);
  }
}

That's pretty much it; the remainder of the code in the page is there for debugging purposes; it allows you to simulate a new Content ID, look at your existing cookies, and delete those cookies again so you can rate an existing item a second time.

I hope this article and the updated control sheds some light on the control's intended usage. I am sure many of you will appreciate the bug fixes..... ;-)

If you have questions, remarks, or found another bug: please let me know.

Download Files


Where to Next?

Wonder where to go next? You can read existing comments below or you can post a comment yourself on this article .


Consider making a donation
Please consider making a donation using PayPal. Your donation helps me to pay the bills so I can keep running Imar.Spaanjaars.Com, providing fresh content as often as possible.



Feedback by Other Visitors of Imar.Spaanjaars.Com

On Thursday, July 17, 2008 5:25:17 PM Jeff said:
Nice one Imar. Well written and informative as always. Thanks for the updated files. I'm looking forward to getting a chance to play around with this.

Any chance you'll be doing an article on this talk back feature (including the CAPTCHA) anytime soon?
On Thursday, July 17, 2008 8:42:27 PM Imar Spaanjaars said:
Hi Jeff,

Thank you for your kind feedback.

Yes, I've been planning to do an article on the Talk Back feature. However, the production code is quite old (almost four years: http://imar.spaanjaars.com/QuickDocId.aspx?quickdoc=320) so I am sure it needs to be redone; especially for an article.

I am planning to redo the feature (and write an article about) using LINQ. I might also kill the Captcha feature as it's difficult to use for many users, especially blind or visually impaired users.

Until then, take a look here to learn how the Captcha is implemented: http://imar.spaanjaars.com/QuickDocId.aspx?quickdoc=311.

Cheers,

Imar
On Friday, July 25, 2008 3:09:14 AM Satheesh said:
Wow!! You are great! Thanks for the next version! I am going to add you to my favourites links on my site!

Regards,
Satheesh
www.codedigest.com
On Thursday, August 07, 2008 1:54:39 PM Samir Nigam said:
Hi Imar . Thanks for this nice control. I want to know what is basic defferent between custom control development model?
On Thursday, August 21, 2008 8:14:22 AM Imar Spaanjaars said:
Hi Samir,

To explain a difference, you need two sides of the equation. In your case you're only providing one: "custom control development model". What is it exactly that you are asking?

Cheers,

Imar
On Tuesday, September 09, 2008 1:37:19 AM Jeff said:
Imar,
There's a typo in your article title... You have "...Toolit..." and I think you wanted Toolkit. You're missing the 'k'.
On Tuesday, September 09, 2008 6:41:13 PM Imar Spaanjaars said:
Fixed.... Thank you!

Imar
On Sunday, December 14, 2008 5:02:33 AM Er said:
Hi Imar,

Great job on the control. I do have a question.
Is there a way to embedd the control on a repeater? I have a list of hotels that I want to show ratings for, and in the Repeater1_ItemDataBound event I want to get the control for each row and set the datasource, but somehow that is not working.

If it is possible, would you have a example of how to do that? If you don't have an example, would you have any tips about how to get it done?

Thanks in advance
On Sunday, December 14, 2008 2:56:44 PM Imar Spaanjaars said:
Hi Er,

Here's a quick sample site using the Rating conttrol in a Repeater:

http://imar.spaanjaars.com/Downloads/Articles/ContentRating/ContentRatingWithRepeater.zip

It's a bit messy as it requires handling ItemCreated and ItemDataBound, but so far it seems to work fine. Not sure why DataBound needs to be handled as well; without it, the control looses its state like ItemId.

Hope this helps,

Imar
On Thursday, February 25, 2010 11:26:07 AM Ellie said:
Hi Imar,
The rating control is great as well as the articles. Thank you for your work.
I have the control implemented in a big project which uses many master pages and user controls. When the rating control is used on a page that does not inherit from any master pages all works ok. However when I put it in user control and then in a page that uses master page it does not work. I investigated the problem and discover that onclick=javascript:WebForm_DoPostBackWithOptions(new WebForm_PostBackOptions("ctl00$PlaceHolderContent$ArticleContents1$ContentRating1$Image1", "", true, "", "", false, false)) is generated and the control does not fire its Rating and Rated events. I tried to get rid of the onclick attribute using javascript (document.getElementsId('ctl00_PlaceHolderContent_ArticleContents1_ContentRating1_Image1').removeAttribute('onclick');) and any other ways I could find in the web but I cannot remove it :(
Can you help me please??
Thank you
Ellie
On Thursday, February 25, 2010 11:38:24 AM Imar Spaanjaars said:
Hi Ellie,

Why are you trying to remove the click handler? How's the control supposed to post back then?

Also, it's getElementById, not getElementsId

Cheers,

Imar
On Thursday, February 25, 2010 11:59:17 AM Ellie said:
Hi Imar,
Thank you so much for the answer.
I downloaded the sample from the article and then compare with mine using FireFox firebug tool. In the working example the images are generated as input tags and have id, style and src attributes. In the not working example they have id, style and src and onclick attributes. In firebug when I delete onklick attribute all is working fine, but I cannot remove it permanently in my code.
Thank you
Ellie
On Thursday, February 25, 2010 12:03:08 PM Imar Spaanjaars said:
It's been a while since I wrote this control so I am not sure I understand what you're talking about. So in my example there's no onclick and in yours there is? Then isn't it better to remove it server side (or not add it) rather than client side? Or am I missing something? You're not being very specific with providing information....

Cheers,

Imar
On Thursday, February 25, 2010 12:26:04 PM Ellie said:
Hi Imar,
This is correct, in your example there is no onclick. The thing is that in my example the onclick is generated dynamically, so I am not sure from where it comes :( I keep investigating it... It may be because of the influence of other scripts and controls on the page ... If I put my example in an empty page the onclick is not generated and all works fine. But when I put it in a control that is used on a page that uses a master page it generates somehow the onclick attribute. I thought that it will be easier to get rid of that onclick with javascript and used code like - document.getElementById('ctl00_PlaceHolderContent_ArticleContents1_ContentRating1_Image1').removeAttribute('onclick'); or document.getElementById('ctl00_PlaceHolderContent_ArticleContents1_ContentRating1_Image1').onclick=null; which does not work. I want to remove the onclick attribute but cannot find a way to do it …
On Thursday, February 25, 2010 12:43:40 PM Imar Spaanjaars said:
Hi Ellie,

Removing the handler by using onclick=null should do the trick. However, since you're being a bit vague with comments such as "does not work", I cannot really recommend much to resolve this issue.

If you want to continue this discussion, can you please post here: http://p2p.wrox.com/index.php?referrerid=385

That makes it easier to share code. If you do post there, be sure to be *specific* and provide lots of detail. "Does not work" is not going to get you anywhere. Explain what does and doesn't work, whether you get errors and if so which ones; what behavior you're seeing etc etc.

Imar
On Friday, February 26, 2010 8:43:24 AM Ellie said:
Hi Imar,
I found the problem :))) Thank you so much for your help! I had .aspx page that was using many user controls as well as master page and it was really difficult to debug. The problem was in 1 of the user controls that my page was using. It had a button with onclick, several text boxes that used RequiredFieldValidator, RegularExpressionValidator and ValidationSummary. Setting the ValidationGroup for all of them solve the problem!
Again thank you for your work and patience with me ;)
All the best of luck
Ellie
On Friday, February 26, 2010 8:45:41 AM Imar Spaanjaars said:
Hi Ellie,

Excellent. Good to hear you got that sorted out.

Cheers,

Imar
On Monday, March 22, 2010 2:27:54 PM Martin said:
Hi Imar,
I'm trying to hook up your control with my linq datacontext. I don't understand how you are pulling in the "contentId"? Your code ref:

// Get the item through LINQ. SingleOrDefault returns NULL when the item is not found.
  Rating result = (from ra in db.Ratings
                  where ra.ContentId == contentId
                  select ra).SingleOrDefault();

I get the error "contentId does not exist in the current context". My ContentId is of type uniqueidentifier, does this make a difference? Also, when you're mapping your stored procedure to a method, is it the insert or update method? Or do I just drop the stored procedure in my linq method pane and leave it at that?

Thanks.
On Monday, March 22, 2010 2:33:56 PM Imar Spaanjaars said:
Hi Martin,

You may want to take a look at the downloadable source, and at Default.aspx.cs specifically. ContentId is a page scoped property I created that uses ViewState as the backing store. It is set (with a random value) and read by a number of methods. In your case, it could come, for example, a QueryString value.

And yes, you just drop the procedures in the Methods pane.

Cheers,

Imar
On Monday, March 22, 2010 3:09:25 PM Martin said:
Thanks for your prompt reply Imar,
I now see how you are randomly generating the ContentId, thanks.

Your control is great but along with allowing users to rate, can it also just be viewable, i.e. is there some way to disable your control so users can just view the rating and not interact with it? You see, I want to hook up another column in my db to your control so only users linked to this column will be able to rate, I don't want every user to be able to rate.

Thanks.
On Monday, March 22, 2010 3:11:14 PM Imar Spaanjaars said:
Hi Martin,

Not directly. However, it shouldn't be too hard to add an additional property and hide the voting controls under certain circumstances.

Cheers,

Imar
On Monday, March 22, 2010 3:23:21 PM Martin said:
Ok Imar, I'll give it a bash....wish me luck!

Thanks again!
On Friday, March 26, 2010 4:25:14 PM Martin said:
Hi Imar,

I have tried to configure your rating control within the itemtemplate of my asp.net listview control. For some reason I keep getting the error "Can't bind a datasource without a valid ItemId" within the onselecting event of the linqdatasource attached to my listview, which doesn't reference your control at all? I have placed your code to build the rating array and assign your control's attributes within the ItemDataBound event of my listview but as I said, this event doesn't seem to be getting fired due to the onselecting error that I'm receiving?

Thanks.
On Saturday, March 27, 2010 2:32:36 PM Imar Spaanjaars said:
Hi Martin,

Difficult to say without seeing your full code. Can you post the relevant code here and send me a link?

http://p2p.wrox.com/index.php?referrerid=385

Cheers,

Imar
On Monday, March 29, 2010 4:35:58 PM Martin said:
Thanks for your reply Imar.

I have posted my code at the below link as suggested...

http://p2p.wrox.com/asp-net-3-5-basics/78736-how-do-i-pre-populate-frontend-control-using-asp-net-c.html

Thanks.
On Saturday, April 10, 2010 8:53:45 PM DS said:
Hi Imar,

I appreciate this article (and your beginning asp.net 3.5 book).  I am a beginner in the truest sense.  

I would like to use your rating control on my site to rate different items.  I see in the example that you are using randomly generated ids and using viewstate.

Could you give me an example of how to set up this control to rate two different items on the same page?  I saw your reference to querystring, but I'm not that familiar with this.  

Thanks in advance!
On Sunday, April 11, 2010 8:04:51 AM Imar Spaanjaars said:
Hi DS,

Take a look at page 259 and further of my book to learn how to use the query string.

You can then assign the ItemId from the query string like this:

string contentId = Request.QueryString.Get("ContentId");
ContentRating1.ItemId = contentId;

Hope this helps,

Imar
On Tuesday, January 24, 2012 3:50:55 AM Srinivasa prabhu.N said:
Nice and informative.Easy to implement.

Talk Back! Comment on Imar.Spaanjaars.Com

I am interested in what you have to say about this article. Feel free to post any comments, remarks or questions you may have about this article. The Talk Back feature is not meant for technical questions that are not directly related to this article. So, a post like "Hey, can you tell me how I can upload files to a MySQL database in PHP?" is likely to be removed. Also spam and unrealistic job offers will be deleted immediately.

When you post a comment, you have to provide your name and the comment. Your e-mail address is optional and you only need to provide it if you want me to contact you. It will not be displayed along with your comment. I got sick and tired of the comment spam I was receiving, so I have protected this page with a simple calculation exercise. This means that if you want to leave a comment, you'll need to complete the calculation before you hit the Post Comment button.

If you want to object to a comment made by another visitor, be sure to contact me and I'll look into it ASAP. Don't forget to mention the page link, or the QuickDocId of the document.

For more information about the Talk Back feature, check out this news item.