Creating an Up and Down Voting User Interface

Published: 3/25/2011
By: Scott Mitchell

This article shows how to build an up or down voting system using ASP.NET and jQuery.

Contents [hide]

Introduction

The Internet is awash in user-created content. Two billion videos are watched on YouTube every day. Over 3,000 images are uploaded to Flickr each minute. And there are an estimated 152 million bloggers creating content each day. Search engines work hard to discover and assign a rating to each of these pieces of user-created content so that they can provide optimal search results. But rating user-created content is not just for search engines – it's also useful for websites that accept user-submitted content.

Consider a question and answer website like Stackoverflow, whose content is the millions of questions asked and answered by users visiting the site. Really interesting questions and answers should be marked as such; questions or answers that are offensive or off-topic should be similarly noted. By rating the questions and their answers, a user visiting the site can quickly see what questions are of interest and what answers should receive the focus of their attention. So how does Stackoverflow go about rating its millions of questions and answers? Given the volume of questions and answers it would be impractical to assign this task to employees. Instead, Stackoverflow turns to its community, allowing users to rate questions and answers as they see fit.

Stackoverflow lets its users rate questions and answers using an up or down voting system. With this system, for each question and for each answer a Stackoverflow user can either: abstain from voting (0); cast an up vote (+1); or cast a down vote (-1). The cumulative vote score – the sum of all up votes less the negative votes – is displayed next to each question and answer, providing a quick indication of the quality of a partiuclar question or answer.

This article shows how to build an up or down voting system using ASP.NET and jQuery. We'll build a website that allows users to share their favorite online articles. The website's homepage lists the 25 most recently shared articles, their current vote score, and arrows for casting an up or down vote.

Download the Demo!

This article's download includes two working demos – an ASP.NET WebForms website and an ASP.NET MVC application. However, any code presented in this article is from the WebForms demo.

I suggest downloading the demo and having it open in Visual Studio as you read through this article.

Creating the Needed Database Tables

Any website that lets users rate content must have, at minimum, three associated tables: one that contains a record for each user in the system; one that contains a record for each content item; and one that records how a particular user has rated a particular content item.

For our website let's use ASP.NET's Membership system to manage user accounts. The demo application have been configured to support forms authentication and to use the SqlMembershipProvider provider; if you are following along at your computer, refer to my tutorial Creating the Membership Schema in SQL Server for instructions on how to implement this configuration. The SqlMembershipProvider provider stores user account information in the aspnet_Users database table; each user is uniquely identified by their UserId, which is of type uniqueidentifier.

The Articles table contains a record for each user-submitted, recommended article, and has the following columns:

Table 1: The Articles table

Field

Data Type

Notes

ArticleId

int

An IDENTITY column; is the primary key.

Title

nvarchar(256)

The recommended article's title.

Url

nvarchar(256)

The recommended article's URL.

VoteScore

int

Stores the article's cumulative vote score.

DateAdded

datetime

The date and time the article was submitted.

The Votes table records each user's votes. Votes establishes a many-to-many join between aspnet_Users and Articles, since each user can vote for any number of articles and each article can have votes from any number of users. The Votes table has the following columns:

Table 2: The Votes table

Field

Data Type

Notes

ArticleId

int

A composite primary key constraint exists on the ArticleId and UserId columns.

UserId

uniqueidentifier

 

VoteValue

int

Stores the user's vote for this article and must be a value of either +1 or -1. There is a CHECK CONSTRAINT in place to enforce this business rule.

DateAdded

datetime

The date and time the vote was cast.

Figure 1 shows these three tables and their relationships.

Figure 1: An up and down voting user interface requires at least three tables in the data model.

An up and down voting user interface requires at least three tables in the data model.

Calculating an Article's Cumulative Vote Score

The Articles table's VoteScore column reports the cumulative votes for each article. However, it is redundant as an article's cumulative vote score can be computed by running a subquery on the Votes table like so:

Listing 1: Query to compute an article's cumulative vote score.

The reason I decided to denormalize the Articles table by adding the VoteScore field was because an article's vote score is needed very often, namely anytime the list of articles is displayed on the homepage. By storing the vote score in Articles I save the database from needing to recompute it each time someone visits the homepage. Admittedly, this is an example of an anti-pattern – premature optimization.

The downside of storing the cumulative vote score in the Articles table (aside from being a premature optimization) is that the stored cumulative vote score could become out of sync with the actual vote score. To ensure that the stored vote score reflects the article's true vote score we need to make sure to update the Articles table's VoteScore field whenever a vote is cast.

The database includes a stored procedure named RecalculateArticleVoteScore that updates (and returns) a particular article's cumulative vote score. RecalculateArticleVoteScore is invoked by the website whenever a user's vote is logged.

Listing 2: The RecalculateArticleVoteScore stored procedure recalculates an article's cumulative vote score.

Accessing the Database from the ASP.NET Application

The demo applications available for download share a common Class Library project named Models. The Models library contains a LINQ to SQL file named Articles.dbml that defines the classes that model the three database tables we examined eariler.

The Models library also contains a class named ArticleRepository.cs, which has three methods:

The demo applications interface with the Models library's ArticleRepository class rather than communicating directly with the LINQ to SQL DataContext class.

The Article class created by the LINQ to SQL OR/M does not include a property to track the currently logged on user's voting record. To capture this information I created a new class named ArticleWithVotingInfo that has properties that specify article-related information – ArticleId, DateAdded, Title, Url, and VoteScore – as well as a property that indicates how the currently logged on user voted for the article in question – UsersVoteValue.

Figure 2: The ArticleWithVotingInfo class diagram.

The 

ArticleWithVotingInfo class diagram.

The UsersVoteValue property is a nullable integer. When the articles are being displayed for an anonymous user, or if the logged in user has not voted for a particular article, then UsersVoteValue will be null. However, if the articles are being viewed by a logged in and that user has voted for a particular article then that article's UsersVoteValue property reports the user's vote – either a +1 or -1.

The ArticleRepository class's GetRecentArticles method returns an enumeration of ArticleWithVotingInfo objects.

Displaying the List of Recommended Articles

The list of recommended articles are displayed in the website's homepage, Default.aspx. A ListView control renders a two-column <table> with a row for each recommended article. The left column of each row displays the voting user interface and the article's cumulative vote score. The right column shows the details about the article, including its title and the date and time it was submitted. Figure 3 shows the output of the ListView when viewed through a browser.

Figure 3: The list of the most recent recommended articles.

The list of the most recent recommended articles.

Before we examine the ListView's markup, let's first look at the code used to populate the ListView. As noted earlier, the ArticleRepository's GetRecentArticles method is used to get the list of recent articles. In the Page_Load event handler we bind the results of the GetRecentArticles method to the ListView.

Listing 3: The results of the GetRecentArticles method are bound to the ListView.

Note that if an anonymous user is visiting the page then we pass in a null value for the GetRecentArticles method's first input parameter, userId. However, if the user visiting is authenticated then we retrieve their UserId from the Membership system and pass this value in. Also note that the user interface in this demo application is not setup to allow the user to page through the most recent articles to see older entries. The call to the GetRecentArticles method does not specify the page or pageSize attributes, meaning that this method call will always return the 25 most recent articles. With a little bit of effort, however, you could add a paging user interface, allowing users to view older recommended articles. For more information along this vein, see The Ultimate DataPager Interface if you are using WebForms and Displaying a Paged Grid of Data in ASP.NET MVC if you are using MVC.

The markup for the ListView follows. The LayoutTemplate starts by rendering a <table> tag. Then, for each ArticleWithVotingInfo object returned by the GetRecentArticles method a table row is rendered. The left column renders the voting user interface while the right column renders information about the article.

Listing 4: The ListView renders a table row for each recommended article.

Let's take a deeper look at the voting user interface column. First, note the <div> element, which displays the cumulative vote score for the article. In Figure 3 this is the number positioned between the up and down arrows. For example, the Customizing ELMAH's Error Emails article has a cumulative vote score of 3.

The up and down arrows are rendered by the DisplayUpVoteArrow and DisplayDownVoteArrow methods, which are defined in the code-behind class. These methods emit a <div> element with the following structure:

The articleId value reports the article's ArticleId value.

For every voting arrow, the <div> element's class attribute value specifies the voteArrow class. Furthermore, every up voting arrow specifies the up class, whereas every down voting arrow specifies the down class. If the user viewing the page has already voted for an article then either the upvoted or downvoted class is also specified.

The up, down, upvoted, and downvoted classes each define a different background image:

The CSS rules for these four classes is displayed below; in the demo, this CSS can be found in the VotingUI.css file.

Listing 5: The CSS rules for the up, down, upvoted, and downvoted classes

Improving Performance with CSS Sprites

The up, down, upvoted, and downvoted classes in the demo application each define their own background image, which means when a user makes an up vote or down vote for the first time the associated image needs to be downloaded by the browser. Moreover, when loading a page with both up votes and down votes the browser must download four separate images.

A more elegant and better performing solution is to use CSS sprites. With CSS sprites all four images are stored in one image and CSS rules are used to selectively display a portion of that master image. Such an approach requires only one image to be downloaded from the server.

For more information on implementing CSS sprites in an ASP.NET application, see Optimize Images Using the ASP.NET Sprit and Image Optimization Framework.

Figure 3 shows all four of these arrows in use. For instance, the How to add a custom dictionary in Word recommended article displays both black up and down arrows, indicating that the logged on user has not yet cast his vote for this article. The markup for these up and down voting arrows follows.

In Figure 3, the up arrow for Customizing ELMAH's Error Emails is green, indicating that this user cast an up vote. The markup for this article's up and down arrows follows. Note that regardless of whether an article has been voted on, the up and down classes are always present. If a vote has been cast, then the upvoted (or downvoted) class is also included.

As you may have surmised, the upvoted and downvoted classes are specified depending on the value of the UsersVoteValue property for the article in question. If UsersVoteValue equals +1 then the upvoted class is included in the up arrow <div> element, otherwise it is omitted. Similarly, if UsersVoteValue equals -1 then the downvoted class is included in the down arrow <div> element, otherwise it is omitted.

Whenever an authenticated user clicks one of the voting arrows we need to record their vote. This is a two step process. First, clicking a voting arrow must run some JavaScript that determines what vote to cast – the user may be voting up or down for the first time or they may be changing or recalling their vote. The browser then needs to send the user's vote to the web server so that it can be recorded. The next two sections look at implementing this functionality.

Casting a Vote When a Vote Arrow Is Clicked

Whenever a user clicks one of the voting arrows we need to record her vote. At first blush, this may seem straightforward – if the user clicks the up arrow then we record a vote of +1, but if she clicks the down arrow then we record a vote of -1. While this is true when a user votes for a particular article for the first time, things are a slightly more complex when a user wishes to change or recall her vote on a previously voted-on article.

In Figure 3, the user has downvoted the "Learn all about ASP.NET!" article as evidenced by the red down arrow. The user may decide to change her vote by clicking the up arrow. At that point we need to update her previously recorded vote from a -1 to a +1. Similarly, she may decide to recall her downvote, which she can do by clicking the down arrow. In this case we want to delete her previously recorded vote.

After a user has voted we also need to update the article's cumulative vote score. For instance, if an article has a cumulative vote score of 6 and a user up votes it, the score should be updated to the new cumulative score. This new score may be 7, but it may some other number if others have also voted on this article in between the time the user loaded the page and placed her vote.

This logic – determining the user's vote, submitting the vote to the server, and updating the cumulative vote score – is handled via client-side script using the jQuery library. The following script defines a client-side event handler for the voting arrows on the page. Note that this event handler is executed whenever any voting arrow on the page is clicked. That is, the same code is executed whether an up arrow or down arrow is clicked or whether the vote is cast for the first article, the second, the third, et cetera.

Voting and Anonymous Visitors

The script presented in this article is the script that is emitted for authenticated users. Anonymous users cannot vote. If an anonymous user visits the site, the ASP.NET page emits different script for the voting arrow click event handler, namely script that displays a modal popup explaining that the user needs to log in before they can vote.

In particular, anytime a vote arrow is clicked the vote function is executed and passed a reference to the particular element that was clicked. (The vote function resides in a separate file, ~/Scripts/vote.js.)

Listing 6: The vote function is called whenever a voting arrow is clicked.

The vote function, shown below, starts by reading in the clicked voting arrow's articleId attribute, which is needed when submitting the vote to the server. Next, we need to determine whether the arrow that was just clicked was an up or down arrow – if the arrow specifies the CSS class up then an up arrow was clicked, otherwise a down arrow.

If a user has already down voted an article and then decides to change his vote to an up vote, we need to both add the upvoted CSS class to the up arrow and remove the downvoted class from the down arrow. For this reason, we need to have a reference to the other vote arrow. That is, if the user clicked the up arrow we also need a reference to the down arrow; if the down arrow was clicked, we need a reference to the up arrow. This other arrow reference is stored in the otherVoteIcon variable.

We also need a reference to the <div> element that displays the cumulative votes for the article so that we can update the article's vote score after the vote is cast. This reference is stored in the totalVotesDiv variable.

Once the articleId attribute has been read and we've determined whether the user clicked the up or down arrow and we have references to the vote score <div> elements and to the other vote arrow, we're ready to update the vote arrow CSS classes to reflect the user's vote.

For example, if the user just clicked the up arrow then we want to toggle its upvoted CSS class. Toggling adds the upvoted class if it doesn't exist, which is the case when up voting an article that either had not been voted on or had been down voted. Toggling removes the upvoted class if it already exists, which implies that the user had already up voted this article. Removing the upvoted class returns the arrow back to its default, non-voted state (a black up arrow as opposed to a green one). In addition to toggling the upvoted class, we also remove the downvoted class (if it exists) from the down arrow. This ensures that if the user had down voted the article and then chose to up vote it, we both add the upvoted class to the up arrow and remove the downvoted class from the bottom arrow.

At this point, the user interface has been updated to reflect the user's vote, but we still haven't sent the vote to the server. Before we can do this we need to determine the vote value to send to the server. Did the user up vote this article (+1), down vote it (-1), or do they want to recall their vote (0)?

The vote is cast by making an asychronous HTTP request to a vote service on the web server. In the WebForms demo this is handled by the Vote.ashx HTTP Handler, which we examine in the next section. In the MVC demo this is handled by the VoteController's Index action. The server-side service returns a JSON payload that defines an object with a property named NewVoteScore, which is the new cumulative vote score for the article. This value is used to update the article's total score displayed in the browser.

Listing 7: The vote function updates the user interface and submits the vote to the server-side service.

Saving a User's Vote to the Database

When a user clicks a voting arrow an HTTP request is sent to server-side voting service, passing along the articleId and the voteValue values. The user's identity is also passed to the server via the forms authentication ticket cookie. With these three pieces of information we can add, update, or delete the user's vote.

The following code, taken from the Vote.ashx HTTP Handler, starts by reading in the articleId and the voteValue values from the querystring and the UserId value of the currently logged in user. Next, it calls the ArticleRespository class's Vote method, which records the vote in the database and returns the new cumulative vote score for the article in question.

The article's cumulative vote score is returned as a JSON payload to the client. Specifically, an object with a single property – NewVoteScore – is serialized using Microsoft's JavaScriptSerializer class, which converts the object into JSON.

Listing 8: The server-side voting service records the vote and returns the article's new cumulative vote score as JSON.

The ArticleRespository class's Vote method is fairly straightforward. It starts by retrieving the current voting record for this article and user. If no such record exists then a new vote is recorded. However, if there is a recorded vote than it is either deleted or updated, depending on the vote value: if the user is recalling their vote then the vote is deleted; if they are chainging it from an up vote to a down vote, or vice versa, then the existing vote is updated.

After logging the vote the RecalculateArticleVoteScore stored procedure is called. This recomputes the article's cumulative vote score, updating the Articles table's VoteScore column. The new cumulative vote score is also returned from the stored procedure, which is then returned from the Vote method.

Listing 9: The ArticleRespository class's Vote method records the user's vote.

For more information on using jQuery to make asynchronous HTTP requests from the client to the server, and for options on how to create server-side services that accept such requests and return data back to the client, see my article series, Accessing Server-Side Data from Client Script.

Conclusion

Ranking content and separating the signal from the noise is an important aspect for any website that accepts user-generated content. While you or an editorial team can rank content on your own, such a policy does not scale. Instead, you'll need to turn to your users. An up and down voting system is a simple and intuitive mechanism for letting the community rank content on your website.

This article walked through building an up and down voting system using ASP.NET and jQuery. In short, users are presented with up and down arrows that, when clicked, execute JavaScript that submits their vote to the web server and updates the user interface to show their cast vote and to update the article's cumulative vote score.

Happy Programming!

Further Reading

Please visit the link at the below url for any additional user comments.

Original Url: http://dotnetslackers.com/articles/aspnet/Creating-an-Up-and-Down-Voting-User-Interface.aspx