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:
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:
In addition, I have modified Global.asax so that the route ~/DoVote/{name}/{id} is directed to the VoteController
:
routes.MapRoute(
"VoteButton", "DoVote/{uniqueName}/{voteId}", new { controller = "Vote", action = "DoVote" },
new { voteId = @"\d{1,3}" } );
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 div
s for the different views:
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
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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