Add a Quick Poll to Your Web Site
Duncan Mackenzie
Microsoft Developer Network
August 3, 2004
Applies to:
Microsoft Visual Basic .NET
Summary: Duncan Mackenzie describes his process to build a "Quick Poll" using Visual Basic and ASP.NET. (16 printed pages)
Download the source code for this article.
Time for Something Different...
Just the other day, at exactly 10 PM, I was doing what I am always doing at 10 PM, sitting on the couch with my wife, Laura. That night had been like every other. We started dinner at 6:33 PM and finished at 7 PM. Dinner was the usual: fish sticks and fries, just like every night. So there we were, each with our tea, like we always do: mine in the blue cup, Laura's in the white cup. She was on the right side of the couch, I was on the left, just like every night. Suddenly, unexpectedly, Laura lets out a sigh and says, "We should do something different one of these days."
Well, I knew exactly what she meant... I leapt up from the couch and rushed off telling Laura not to worry, something different would be in the works very soon. Yep, it was certainly time for a change...
So I decided to write a Web application. I bet she'll be ecstatic.
I almost decided to write it in C#, but just venturing outside of Microsoft Windows Forms is enough for now. If you hadn't noticed, almost every article—okay, every article—in the Coding4Fun series has been about Windows Forms. Now Laura doesn't read these articles, so I'm a little mystified how she knew I had been sticking to a standard routine, but a good husband knows when to take a hint, regardless of the context.
Deciding what to build took very little time at all. I wanted to be able to host "Quick Polls" on both my personal Web site and hopefully on MSDN. A Quick Poll, if the term isn't already familiar to you, is a simple multiple choice question (see Figure 1) that is often used on Web sites.
Figure 1. Quick Polls are usually multiple-choice and are often used for casual surveys.
People vote (select an option) and then they get to see the tabulated results of all the other votes that have been cast, including their own (see Figure 2).
Figure 2. The real fun of a quick-poll is seeing how everyone else voted
For various reasons, this is not the best way to conduct a "real" survey, and the results obtained are at least slightly questionable, but it is a fun and interesting bit of interactive content to offer on your Web site. I won't go into detail here about why this method isn't perfect, but you can read about it on your own by searching the web for "SLOPS", "Self-Selected Surveys," or read a brief description in What Type of Survey is Best? by Carolyn Browne.
Starting with Someone Else's Code
As a programmer, I generally want to build the cool stuff, but you should always at least consider buying (or downloading) part or all of the code you need to accomplish a project. Price out a few commercial options, investigate any free examples available and then determine if any of these resources meet your specific requirements and fall within your budget. I looked around a bit and found that Rob Howard had already written a simple polling component for use on the Microsoft ASP.NET Web site. This component, available from here, was very easy to use and the code was very clear. It was the perfect place to start experimenting.
Within a couple of hours, I had figured out how to access the Microsoft SQL Server that my Web host provides me with, set up the one table (see the SQL statement below) that Rob's component uses, and create the poll.xml file that described the poll I wanted to run.
CREATE TABLE [Poll] (
[PollName] [nvarchar] (128) NOT NULL ,
[Vote] [char] (1) NOT NULL ,
[VoteCount] [int] NOT NULL
) ON [PRIMARY]
I set everything up on my laptop first, creating a mirror of my public site and the SQL database, tested it in the most casual fashion, then I copied the component up to my public site and modified the code of my default.aspx page to display it right near the top of the page on http://www.duncanmackenzie.net.
This seemed to work well, but very quickly I realized I wanted to add a feature. Wouldn't it be useful if people could choose to view the results without actually voting? I would hate to imagine people voting, perhaps without giving their answer much thought, just because they are interested in seeing how the poll is going. Going into Rob's C# code, I added a new "Show Results" button and a little bit of code to make it switch views without voting. I uploaded the modified control and almost instantly received the feedback that once a user picked "Show Results," there was no way to get back to the voting view without closing and reopening your browser. Okay, so I added a "Go Back" button to the view of the page that shows the Results. It was relatively easy to implement.
Note I will be going through the code of the final control in more detail later in this article, including the new features I just described. And, of course, all of the code is available for download, so anything left out of the article will be there in the source.
That was the last of the UI modifications I wanted to make—the control was now behaving exactly as I had hoped—but I started to realize that a much larger rewrite was coming. The data structure of the control, with the poll details stored in an XML file and the votes themselves stored in a database table, left a bit to be desired (a comment that I will explain in just a moment). I also had a set of additional requirements in mind that would necessitate even more changes.
Back to the Drawing Board
Any project, even an informal one like this, can benefit from writing down the requirements for the finished work. In this case, I wanted a Quick Poll component that would allow me to:
- Schedule polls for different locations on a Web site.
- Retrieve results for any poll (whether it was still active or not).
- View results broken down by poll location and date range.
The original design of the component didn't allow for any of these advanced features. In particular, since the only place the poll's choices were stored was the poll.xml file, once a new poll was running, their was no structured way to link people's votes with the choices they were voting for. I decided to remove the xml file from the system, move everything into the database, and add my additional functionality as I designed that new data storage system. I started with an empty database and created the following tables:
- Poll—To store the main details, including the question, an ID, a Name, and the date the Poll was created).
- PollDates—For scheduling polls into a specific location between two dates.
- Location—List all valid locations for scheduling.
- PollOptions—The list of choices for a specific poll.
- Vote—Contains the actual vote submitted by a user, including their IP address, the poll they were voting in, the choice they made, and the location from which the poll was accessed.
Figure 3. Database Diagram for the new polling system
I chose to use integer IDs for those items that I will need to specify in the properties of the control, as no one likes to copy and paste GUIDs around any more than they have to. I did use a GUID for the Vote records though, since these will represent all votes over the entire lifetime of this system. With the tables in place, I created four stored procedures that would represent the core of my interaction with the system:
- GetPollsByLocation
- GetPollDetails
- GetVoteCount
- IncrementPoll
Of course, I will need to add to this list of procedures to support reporting and poll administration (creation/editing), but I'm not going to build that until the component is up and running.
To keep my database work independent from my ASP.NET-related code, I created some simple classes to represent the Poll and its related options.
Figure 4. Simple classes were used to slightly abstract the control from the database
I also used CodeSmith to create three collection classes—Polls, PollOptions and PollResults—all used to hold instances of the appropriate entity. These classes are not really worth detailing in the text of the article (although they are included in the source download) as they are nothing more than storage devices with simple property routines and the appropriate internal variables. I also created two classes to encapsulate all of my database work:
- Loader, which provides access to GetPollsByLocation, GetPollDetails, and GetVoteCount.
- Voter (cheesy name I know), which wraps the IncrementPoll stored procedure.
Here is the code for LoadPoll (which calls GetPollDetails) to illustrate the basic structure of all of these database calls.
Public Shared Function LoadPoll(ByVal pollID As Integer, _
ByVal connectionString As String) As Poll
Dim result As New Poll
Dim conn As New SqlConnection(connectionString)
conn.Open()
Dim cmdGetPollDetails As New SqlCommand("GetPollDetails", conn)
cmdGetPollDetails.CommandType = CommandType.StoredProcedure
Dim paramPollID As SqlParameter _
= cmdGetPollDetails.CreateParameter()
With paramPollID
.SqlDbType = SqlDbType.Int
.ParameterName = "@PollID"
.Direction = ParameterDirection.Input
.Value = pollID
End With
Dim paramPollName As SqlParameter _
= cmdGetPollDetails.CreateParameter()
With paramPollName
.ParameterName = "@PollName"
.Direction = ParameterDirection.Output
.SqlDbType = SqlDbType.NVarChar
.Size = 50
End With
Dim paramPollQuestion As SqlParameter _
= cmdGetPollDetails.CreateParameter()
With paramPollQuestion
.ParameterName = "@PollQuestion"
.Direction = ParameterDirection.Output
.SqlDbType = SqlDbType.NVarChar
.Size = 100
End With
cmdGetPollDetails.Parameters.Add(paramPollID)
cmdGetPollDetails.Parameters.Add(paramPollName)
cmdGetPollDetails.Parameters.Add(paramPollQuestion)
Dim dr As SqlDataReader = _
cmdGetPollDetails.ExecuteReader( _
CommandBehavior.CloseConnection)
If dr.HasRows Then
Dim po As PollOption
Do While dr.Read
po = New PollOption
po.OptionID = dr.GetInt32(0)
po.OptionText = dr.GetString(1)
result.Options.Add(po)
Loop
result.ID = pollID
result.Name = CStr( _
cmdGetPollDetails.Parameters("@PollName").Value)
result.Question = CStr( _
cmdGetPollDetails.Parameters("@PollQuestion").Value)
dr.Close()
Return result
Else
dr.Close()
Return Nothing
End If
End Function
The other routines follow the same basic structure: calling the stored procedure, using a DataReader to return the results, and then turning those results into one of my project's custom object classes. Voting is a bit different, of course, since it doesn't need to return anything, but it is just doing an insert into the Vote table with 4 values through the IncrementPoll stored procedure shown below.
CREATE PROCEDURE dbo.IncrementPoll
(
@PollID int,
@Vote int,
@LocationID int,
@IPAddress nvarchar(50)
)
AS
INSERT INTO
Vote
(PollID, LocationID, OptionID, IPAddress)
VALUES
(
@PollID,
@LocationID,
@Vote,
@IPAddress
)
With my database ready, and with all of my data access code created, it is time to try my hand at building an ASP.NET control.
Building My First ASP.NET Control
There is a first time for everything, and you get to see my first entry into the ASP.NET control world. The one control I had read through in any detail was Rob Howard's Poll component, so the structure and code of my control is based on that example. I started off using the WebControl library project template, which does not allow the creation of .ascx style controls, and created my control as a simple class that inherited from System.Web.UI.WebControls.WebControl. I added the desired properties to my class (for the Poll ID, Location ID, Connection String Key, and three properties for style attributes), and then I started working on the flow for the control.
The control will have two states, either it will be showing the user an option button list (also known as a radio button list) of choices (to see the "choices" layout, see Figure 1), or it will be showing a bar graph of the results (the "results" layout, as in Figure 2). When the control is not being rendered in response to one of its own button clicks, it decides which view to show based on the presence of a cookie. This cookie is set when you vote. It serves two purposes:
- To improve the user experience by showing them the results immediately if they have voted on a previous visit.
- To reduce the amount of ballot-stuffing (one person submitting multiple votes).
In the OnInit routine of my control, I check for the cookie and then call either ShowResults or ShowChoices as appropriate.
Protected Overrides Sub OnInit(ByVal e As System.EventArgs)
Dim myCookie As Web.HttpCookie
myCookie = GetCookie()
If Me.PollID <= 0 And Me.LocationID > 0 Then
Me.PollID = Me.GetPollID(Me.LocationID)
End If
If myCookie Is Nothing OrElse _
myCookie.Value = String.Empty Then
Me.ShowChoices()
Else
Me.ShowResults(myCookie.Value)
End If
End Sub
Private Function GetCookieName() As String
If Me.PollID > 0 Then
Return "Poll_" & Me.PollID
Else
Return Nothing
End If
End Function
Private Function GetCookie() As Web.HttpCookie
Dim myCookie As Web.HttpCookie = _
Me.Context.Request.Cookies(Me.GetCookieName)
Return myCookie
End Function
In the actual rendering routines, I am creating my controls and layout manually, as opposed to laying them out visually, as one would when working with an .ascx, which seems very old school. Everything works, though, and the layout is simple enough that it doesn't take that much code to create. To create the "choices" layout, I pull back the poll information, and use data binding to display a list of option buttons with the appropriate text.
Private Sub ShowChoices()
Me.Controls.Clear()
Dim myPoll As PollInformation.Poll
myPoll = GetPoll(Me.PollID)
If myPoll Is Nothing Then
Dim noPoll As New WebControls.Label
noPoll.Text = "No Poll Found"
noPoll.ApplyStyle(Me.ItemStyle)
Me.Controls.Add(noPoll)
Else
Dim questionLabel As New WebControls.Label
questionLabel.Text = myPoll.Question
questionLabel.ApplyStyle(Me.QuestionStyle)
myRB.DataSource = myPoll.Options
myRB.DataValueField = "OptionID"
myRB.DataTextField = "OptionText"
myRB.DataBind()
myRB.ApplyStyle(Me.ItemStyle)
Me.Controls.Add(questionLabel)
Me.Controls.Add(myRB)
voteButton.Text = "Vote"
showResultsButton.Text = "Show Results"
voteButton.ApplyStyle(Me.ButtonStyle)
showResultsButton.ApplyStyle(Me.ButtonStyle)
Me.Controls.Add(voteButton)
Me.Controls.Add(showResultsButton)
End If
End Sub
Instead of calling into my data routines (LoadPoll in this case, described earlier) directly, I call them through some little routines that handle caching this data.
Private Function GetPoll(ByVal id As Integer) _
As PollInformation.Poll
Dim cacheKey As String = String.Format("Poll_{0}", id)
Dim obj As Object
Dim myPoll As PollInformation.Poll
obj = Me.Context.Cache.Get(cacheKey)
If obj Is Nothing Then
myPoll = PollInformation.Loader.LoadPoll( _
id, Me.GetConnectionString)
Me.Context.Cache.Add(cacheKey, myPoll, _
Nothing, Now.AddHours(1), _
Web.Caching.Cache.NoSlidingExpiration, _
Web.Caching.CacheItemPriority.Normal, Nothing)
Else
myPoll = DirectCast(obj, PollInformation.Poll)
End If
Return myPoll
End Function
Note The length of time to cache the data is hard coded at the moment (please restrain your shock), but it would be a good candidate for moving into the configuration or properties of the control in the future.
The final choices layout (see Figure 5) includes two buttons, Vote and Show Results. Show Results just calls the ShowResults procedure without a selected item parameter (which I'll cover in just a bit), and Vote calls back to my database routines to cast the user's vote for a specific option.
Figure 5. The Show Results button was added to allow people to see the current voting data without actually casting a vote themselves.
Private Sub voteButton_Click(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles voteButton.Click
If Not myRB.SelectedItem Is Nothing Then
Dim ip As String = context.Request.UserHostAddress
Dim myPoll As Poll
myPoll = Me.GetPoll(Me.PollID)
If ChoiceInOptions(myPoll.Options, myRB.SelectedItem.Text, _
CInt(myRB.SelectedValue)) Then
Me.SetCookie(myRB.SelectedItem.Text)
PollInformation.Voter.Vote(Me.PollID, _
CInt(myRB.SelectedValue), _
0, ip, Me.GetConnectionString)
Me.ShowResults(myRB.SelectedItem.Text)
End If
End If
End Sub
Note The ChoiceInOptions function just ensures that the vote ID and text is equal to one of the choices I have in the database for the current Poll. Checking those values may be overkill, but it makes it a bit harder to post invalid data into the system through the control.
The results layout displays a bar chart (see Figure 6) using a combination of a table and another ASP.NET control based on Rob Howard's Progress Bar control (included as part of his original Poll component). If the results layout is displayed after the user has voted, the user's choice is passed in as a string (validated against the list of valid options using the ChoiceInOptions function). In the case of a user that has just clicked on the Show Results button (and therefore hasn't voted), an empty string is passed in (and a Go Vote button is shown).
Figure 6. The bar chart is constructed by dividing the area into a bunch of small cells and then setting the back color of the appropriate number of cells.
Private Sub ShowResults(ByVal choice As String)
Me.Controls.Clear()
Dim lbl As New WebControls.Label
lbl.ApplyStyle(Me.QuestionStyle)
Dim myPoll As PollInformation.Poll
myPoll = Me.GetPoll(Me.PollID)
Dim results As PollInformation.PollResults
If choice = String.Empty Then
results = Me.GetPollResults(Me.PollID)
Else
results = Me.GetPollResults(Me.PollID, True)
End If
If myPoll Is Nothing OrElse results Is Nothing Then
Dim noPoll As New WebControls.Label
noPoll.Text = "No Poll Found"
noPoll.ApplyStyle(Me.ItemStyle)
Me.Controls.Add(noPoll)
Else
lbl.Text = myPoll.Question
Me.Controls.Add(lbl)
Dim resultsTable As New WebControls.Table
resultsTable.CellSpacing = 2
resultsTable.Width = WebControls.Unit.Percentage(1)
Dim totalVotes As Integer = 0
If results.Count > 0 Then
totalVotes = results(0).TotalVotes
End If
For Each pr As PollInformation.PollResult In results
Dim tr As New WebControls.TableRow
Dim td As New WebControls.TableCell
td.Text = pr.OptionText
td.ApplyStyle(Me.ItemStyle)
td.Wrap = False
tr.Cells.Add(td)
td = New WebControls.TableCell
Dim pb As New ProgressBar
pb.Percentage = (pr.Result / pr.TotalVotes)
pb.Cols = 20
pb.BackColor = System.Drawing.Color.LightGray
pb.ForeColor = System.Drawing.Color.Blue
td.Controls.Add(pb)
tr.Cells.Add(td)
td = New WebControls.TableCell
td.ApplyStyle(Me.ItemStyle)
td.Text = CInt(pb.Percentage * 100) & "%"
'td.Width = WebControls.Unit.Percentage(0.2)
tr.Cells.Add(td)
resultsTable.Rows.Add(tr)
Next
Dim Summary As New WebControls.TableRow
Dim yourVote As New WebControls.TableCell
yourVote.HorizontalAlign = WebControls.HorizontalAlign.Right
yourVote.ApplyStyle(Me.ItemStyle)
yourVote.ColumnSpan = 3
Me.goBackButton.ApplyStyle(Me.ButtonStyle)
Me.goBackButton.Text = "Go Vote"
Summary.Cells.Add(yourVote)
resultsTable.Rows.Add(Summary)
Me.Controls.Add(resultsTable)
If (Not choice = String.Empty) AndAlso _
ChoiceInOptions(myPoll.Options, choice) Then
yourVote.Text = _
String.Format( _
"Your Vote: {0}<br />Total Votes: {1}", _
choice, totalVotes)
Else
yourVote.Text = _
String.Format( _
"Total Votes: {0}", _
totalVotes)
Me.Controls.Add(Me.goBackButton)
End If
End If
End Sub
There is more code in this project than I wanted to list directly in the article, so you will want to pull down the code and check it out for yourself. Included in the .msi is a SQL Script for the database, including the tables, the stored procedures, and several triggers. A test ASP.NET page is also included, along with a sample web.config file, but you will need to customize it to include your own connection string information. Adding this control to a page involves only two things, a Register statement at the top of the page, and the control itself:
<%@ Register TagPrefix="cc1"
Namespace="QuickVote"
Assembly="QuickVote" %>
<form id="Form2" method="post" runat="server">
<cc1:vote id="Vote2" runat="server"
PollID="-1"
Height="96px"
Width="376px"
LocationID="1">
</cc1:vote>
</form>
In a future article, I hope to extend this sample by moving the database access into a set of Web services. This design change would allow the control to be hosted on other Web sites, and to be extended to rich-client applications. Of course, adding administrative functionality and reporting would also be useful, so perhaps you will see more than one update to this system.
Coding Challenge
At the end of some of my Coding4Fun columns, I include a little coding challenge—something for you to work on if you are interested. For this article, the challenge is to create a Microsoft .NET application around the topic of polling, surveying, or something else related to my sample. Just post whatever you produce to GotDotNet and send me an e-mail message (at duncanma@microsoft.com) with an explanation of what you have done and why you feel it is interesting. You can send me your ideas whenever you like, but please just send me links to code samples, not the samples themselves (my inbox thanks you in advance).
Have your own ideas for hobbyist content? Let me know at duncanma@microsoft.com, and happy coding!
Coding4Fun
Duncan Mackenzie is the Microsoft Visual Basic .NET Content Strategist for MSDN during the day and a dedicated coder late at night. It has been suggested that he wouldn't be able to do any work at all without his Earl Grey tea, but let's hope we never have to find out. For more on Duncan, see his site.