Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

ASP.NET MVC 2 Voting Control

0.00/5 (No votes)
21 Apr 2011 1  
Simple voting control for MVC projects, using a partial view
Vote Control

Introduction

This is a simple voting control for MVC projects. It is implemented as a partial view, and so can be included wherever required.

Main Features

  • Easy to include on any views
  • Question, Answers and Votes stored in XML files (one file per vote control)
  • Configurable style
  • AJAX call to update results
  • Common JavaScript and style sheet for all vote controls

Using the Code

In the source code, I have added two vote controls to the default Index.aspx page. These are included by using the following code in the view:

<%
   VoteDataModel petModel = (VoteDataModel)ViewData[VoteDataModel.GetViewDataName("Pet")];
   petModel.ControlWidth = 200;
   Html.RenderPartial("VoteControl", petModel);
%>

In the above code, we are specifying the vote control name (Pet) and width (200).

In order for this to work, you need to have the following line at the top of the view, just below the page directive:

<%@ Import Namespace="VoteControl.Models"%>

Also, in this case where I have used the default master page, you have to add the stylesheet and JavaScript includes in the head section of the master page:

<link href="~/Content/VoteControl.css" rel="stylesheet" type="text/css" />
<script src='<%= Url.Content("~/Scripts/VoteControl.js") %>' type="text/javascript">
</script>

I included these just after the include for Site.css. If we weren't using a master page, these lines would be included directly on the page.

In the controller, the model has to be constructed and passed to the view using the following code:

VoteDataModel model = new VoteDataModel
			("Pet", Request.PhysicalApplicationPath + "App_Data\\");
model.Open();
ViewData[model.ViewDataName] = model;

The entire page (having voted on both polls) looks like:

Vote Control

Naming Conventions

The name of the vote control is specified in the VoteDataModel constructor. This must be unique, and is used in various ways including for example the XML file name - NamePoll.xml (stored in the app_data directory).

Implementation

The code consists of the following new files:

  • Model - VoteModel.cs
  • Controller - VoteController.cs
  • Partial View - VoteControl.ascx
  • Script - VoteControl.js
  • Style Sheet - VoteControl.css
  • App_Data - XML files containing the vote control data

The files are where you would expect them in the solution, shown below:

Solution Files

In addition, I have modified Global.asax so that the route ~/DoVote/{name}/{id} is directed to the VoteController:

routes.MapRoute(
    "VoteButton", // Route name
    "DoVote/{uniqueName}/{voteId}", // URL with parameters
    new { controller = "Vote", action = "DoVote" },
    new { voteId = @"\d{1,3}" } // voteId can only be numeric, and less than 1000
);

This is used in an AJAX call when the vote button is pressed. The result from this is then used to update the control with the new votes / percentages.

Under the Hood

The bulk of the code is in the model (as it should be).

The VodelDataModel object contains a list of answers (List<AnswerItem>) and IP addresses (List<IpAddressItem>). There is one AnswerItem for each answer, and one entry is added for each unique client IP address. This is how we make sure the user only votes once. It's not perfect - see the "Points of Interest" section for more details about this.

The following public methods are available in the model:

static string GetViewDataName(string uniqueName) and string ViewDataName

This gets a name that will be used for the view data. We use this in the controller, and in the view - there is a static method that is used in the view, and a instance property that is used in the controller.

public VoteDataModel(string name, string path)

Object constructor.

public bool Open()

Open should always be called after constructing the object. This reads the data from the XML file.

public bool DoVote(int voteId, string ipAddr)

Thread safe - see last section.

This updates the object and the file. Returns true if vote was accepted or false if the ip address was found - i.e., the user has already voted.

public int GetPercentage(int index)

This is used in the partial view to display the percentage.

public int GetBarLength(int index, int controlWidth)

This is used in the partial view to display the percentage bar.

Partial View HTML

The control has three divs for the different views:

Vote Control HTML

The first one is displayed by default. The other two are set to display:none in the inline style by default. Depending on which buttons or links are pressed, one of these is set to displayed, and the other two are set to display:none.

JavaScript

I decided to do the AJAX call using raw JavaScript but I could have used jquery (or the MicrosoftAjax.js file). I decided on this approach because I wanted to be as flexible (and transparent) on this as possible. You may want to change this to use jquery - especially if you are using jquery already on existing pages. Note that I have deleted the unused scripts (jquery and MicrosoftAjax) because otherwise it made the download very large!

There are two other functions - these are used to show or hide the relevant controls for either the question, percentage answers, or answers with numbers of votes.

Style Sheet

There are classes for most elements in the VoteControl.css stylesheet - hopefully the names are quite intuitive. In particular, it is likely you will want to change the colors to fit in with the color scheme of your site - for this, note that normal and alternate rows can be (and are in the example) colored differently.

Points of Interest

  1. The control is contained in a DIV, but if you need any special formatting, then the easiest way is probably to wrap the control in another DIV (in the view where the partial view has been included) with additional style attributes applied to it. The example code shows how to float the "pet" control so that it can be embedded within a text section.
  2. In the controller, I construct the model and pass it to the view in ViewData. There are many other ways that this could be achieved - and the way I have done this is not necessarily the best approach. You may want to do this in a way that fits in with your particular project.
  3. Where I have included the css and javascript files, I have used ~/... and Url.Content("~/..."). The reason why these are different is because the CSS include is a server side include and the JavaScript include is a client side include. The important thing here though is that by using "~" the paths to the files will always be correct, regardless of where we are including this code, and it will add the virtual directory if required. It's not uncommon that the virtual directory may be different locally to the hosted version - but this will work with both.
  4. There are two sorts of thread safety that are required. We need to make sure the file isn't written to by two threads at once, or read when it is being written to. This is done with a lock on a static object. We also need to make sure that if when the user selects an item and votes for it - if another user has voted in the meantime - we need to reload the file - otherwise the previous vote will be lost. The first sort of locking is pessimistic locking (we wait until we have exclusive access), the second type is optimistic locking (we reload if there is a conflict). We have to do it like this - we can't lock all other users out - because the user voting is very slow (relatively speaking), whereas reading and writing to the file is very quick.
  5. There is no easy way to prevent people voting multiple times unless they are a registered user. I decided to use the IP address of the client, but this in itself can cause problems and is by no means perfect (a user can vote from multiple machines, multiple machines could be sharing a single IP address, etc.). Also, because of the large number of IP addresses that could accumulate in the XML file, I decided to delete the entries after one day. This means that a determined user could vote every day. I have seen other people using cookies, but again, a determined user could delete the cookie and vote again. So if you really need the vote to be incorruptible, then you have to make people log on as a valid user. Even this is open to abuse if users can register multiple times.
  6. I could have stored the polls in a SQL server database, but I decided to use XML files. This is because not every hosting company gives free SQL server databases. If you want to modify the VoteDataModel so it uses a database, it should just be a simple case of modifying the Open and Save methods. For a more thorough fix, some refactoring to the ParseXML may be required.
  7. I have included the style sheet and JavaScript files in the partial view. However, this is surrounded by if (false) which means that no code will be generated. The only reason for including these files is so that intellisense works.

History

  • Apr 2011 - Initial version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here