This section looks at how you can provide poll functionality for the site. This polling stores the data (questions, answers, votes, etc.) in the database shared by all modules of this book (although the configuration settings do allow each module to use a separate database, if there's a need to do that). To easily access the DB you'll need tables, stored procedures, a data access layer, and a business layer to keep the presentation layer separate from the DB and the details of its structure. Of course, some sort of user interface will allow administrators to see and manage the data using their favorite browser. We'll start with a list of features we want to implement, and then we'll design the database tables, stored procedures, data and business layers, user interface services, and security that we need for this module.
Here's the list of features needed in the polls module:
An access-protected administration console to easily change the current poll and add or remove questions. It should allow multiple polls and their response options to be added, edited, or deleted. The capability to have multiple polls is important because you might want to have different polls in different sections of your site. The administration pages should also show the current statistical results for each poll, and the total number of votes for each poll, as a quick general summary.
A user control that builds the poll box that can be inserted into any page. The poll box should display the question text and the available options (usually rendered as radio buttons to allow only one choice). Each poll will be identified by a unique ID, which should be specified as a custom property for the user control, so that the webmaster can easily change the currently displayed question by setting the value for that property.
You should prevent users from voting multiple times for the same poll. Or, even better, you should be able to dynamically decide if you want to allow users to vote more than once, or specify the period for which they will be prevented from voting again
You can only have one poll question declared as the current default. When you set a poll question as being current, the previous current one should change its state. The current poll will be displayed in a poll box unless you specify a non-default poll ID. Of course, you can have different polls on the site at the same time depending on the section (perhaps one for the Beer-related article category, and one for party bookings), but it's useful to set a default poll question because you'll be able to add a poll box without specifying the ID of the question to display, and you can change the poll question through the administration console, without manually changing the page and re-deploying it.
A poll should be archived when you decide that you no longer want to use it as an active poll. Once archived, if a poll box is still explicitly bound to that particular poll, the poll will only be shown in Display state (read-only), and it will show the recorded results.
We need a page that displays all the archived polls and their results. A page for the results of the current poll is not necessary, as they will be shown directly by the poll box — instead of the list of response options — when it detects that the user has already voted. This way, users are forced to express their opinion if they want to see the poll's results (before the poll expires), which will bring in more votes than we would get if we made the current results freely available to users that have not yet voted. There must also be an option that specifies whether the archive page is accessible by everyone, or just by registered users. You may prefer the second option to give the user one more reason to register for the site.
As discussed in the "Problem" section, we want to be able to control whether users can cast multiple votes, and allow them to vote again after a specified period. Therefore, you would probably like to give the administrator the capability to prevent multiple votes, or to allow multiple votes but with a specified lock duration (one week in the previous example). You still have to find a way to ensure that the user does not vote more times than is allowed. The simplest, and most common and reliable, solution is writing a cookie to the client's browser that stores the ID of the poll for which the user has voted. Then, when the poll box loads, it first tries to find a cookie matching the poll. If a cookie is not found, the poll box displays the options and lets the user vote. Otherwise, the poll box shows the latest results and does not allow the user to vote again. To allow multiple votes, the cookie will have an expiration date. If you set it to the current date plus seven days, it means that the cookie expires in seven days, after which the user will be allowed to vote again on that same question.
Writing and checking cookies is straightforward, and in most cases it is sufficient. The drawback of this method is that the user can easily turn off cookies through a browser option, or delete the cookies from their machine, and then be allowed to vote as many times as they want to. Only a very small percentage of users keep cookies turned off — except for company users where security is a major concern — because they are used on many sites and are sometimes actually required. Because of this, it shouldn't be much of an issue because most people won't bother to go to that much trouble to re-vote, and this is not a high security type of voting mechanism that would be suitable for something very important, such as a political election.
There's an additional method to prevent multiple votes: IP locking. When users vote, their computer's IP address can be retrieved and stored in the cache together with the other vote details. Later in the same user session, when the poll box loads or when the user tries to vote again, you can check whether the cache contains a vote for a specific poll, by a specified IP. To implement this, the Poll ID and user's IP may be part of the item's key if you use the Cache class; otherwise, the Poll ID is enough if you choose to store it in Session state storage, because that's already specific to one user. If a vote is found, the user has already voted and you can prevent further voting. This method only prevents re-voting within the same session — the same user can vote again the next day. We don't want to store the user's IP address in the database because it might be different tomorrow (because most users today have dynamically assigned IP addresses). Also, the user might share an IP with many other users if they are in a company using network address translation (NAT) addresses, and we don't want to prevent other users within the same company from voting. Therefore, the IP locking method is normally not my first choice.
There's yet another option. You could track the logged users through their username, instead of their computer's IP address. However, this will only work if the user is registered. In our case we don't want to limit the vote to registered users only, so we won't cover this method further.
In this module we'll provide the option to employ both methods (cookie and IP), only one of them, or neither. Employing neither of them means that you will allow multiple votes with no limitations, and this method should only be used during the testing stage. In a real scenario you might need to disable one of the methods — maybe your client doesn't want to use cookies for security reasons, or maybe your client is concerned about the dynamic IP issue and doesn't want to use that method. I personally prefer the cookie option in most cases.
In conclusion, the polls module will have the following options:
Multiple votes per poll can be allowed or denied.
Multiple votes per poll can be prevented with client cookies or IP locking.
Limited multiple votes can be allowed, in which case the administrator can specify lock duration for either method (users can vote again in seven days, for example).
This way, the polls module will be simple and straightforward, but still flexible, and it can be used with the options that best suit the particular situation. Online administration of polls follows the general concept of allowing the site to be remotely controlled by managers and administrators using a web browser.
We will need two tables for this module: one to contain the poll questions and their attributes (such as whether a poll is current or archived) and another one to contain the polls' response options and the number of votes each received. The diagram in Figure 6-1 shows how they are linked to each other.
Here you see the primary and foreign keys, the usual AddedDate and AddedBy fields that are used in most tables for audit and recovery purposes, and a few extra fields that store the poll data. The tbh_Polls table has a QuestionText field that stores the poll's question, an IsArchived bit field to indicate whether that poll was archived and no longer available for voting, and an ArchivedDate field for the date/time when the poll was archived (this last column is the only one that is nullable). There is also an IsCurrent bit field, which can be set to 1 only for a single poll, which is the overall default poll. The other table, tbh_PollOptions, contains all the configurable options for each poll, and makes the link to the parent poll by means of the PollID foreign key. There is also a Votes integer field that contains the number of user votes received by the option. In the first edition of the book we had a separate Votes table, in addition to these two, that would store every vote in a separate row, with the date/time of the vote and the IP of the user who cast the vote. I changed the design in this new edition because that additional table made the implementation more difficult and had worse performance (caused by the aggregations you had to run to count the total number of votes for each option), without adding any real benefit.
The stored procedures in the following table will be needed by the poll administration pages.
Property |
Description |
---|---|
tbh_Polls_InsertPoll |
Inserts a new poll with the specified question text. If the new poll must become the new current one, this procedure first resets the IsCurrent field of all existing polls. |
tbh_Polls_UpdatePoll |
Updates an existing poll. If the poll being updated must become the new current one, the procedure first resets the IsCurrent field of all existing polls. |
tbh_Polls_DeletePoll |
Deletes a poll with the specified ID |
tbh_Polls_ArchivePoll |
Archives a poll with the specified ID, by setting its Is archived field to 1, and the ArchivedDate field to the current date and time |
tbh_Polls_GetPolls |
Retrieves all data for the rows of the tbh_Polls table, plus a calculated field that is the sum of all votes for the poll's child options. The procedure takes as input a couple of methods that enable you to specify whether you want to include the active (non-archived) polls in the results, and whether you want to include the archived polls in this poll box. |
tbh_Polls_GetPollByID |
Like tbh_Polls_GetPolls but for a single poll whose ID is specified in input |
tbh_Polls_GetCurrentPollID |
Returns the ID of the poll marked as current, or -1 if no current poll is found |
tbh_Polls_InsertOption |
Inserts a new response option for the specified poll |
tbh_Polls_UpdateOption |
Updates the text for the specified option |
tbh_Polls_DeleteOption |
Deletes an existing option |
tbh_Polls_GetOptions |
Retrieves all data for all options of a specified poll, plus a Percentage decimal calculated field, which is the percentage of each option's votes out of the total number of votes for the poll |
tbh_Polls_GetOptionByID |
Like tbh_Polls_GetOptions, but for a single option |
tbh_Polls_InsertVote |
Inserts a new vote for a specified option, by incrementing the option's Votes integer field |
Most of these stored procedures are straightforward and shouldn't need any further explanation. In fact, they won't even be shown again in the "Solution" section, as they just include some very basic SQL code (their full text is available in the code download, of course). We'll only examine some of the most interesting procedures. Only two procedures are worth special attention: the one that retrieves the polls and vote sums for each child option, and the one that retrieves the options and calculates the percentage of votes for each single option. The two parameters of the tbh_Polls_GetPolls procedures are useful because they enable you to retrieve only the active polls to be shown in the management page, and only the archived page to be shown in the archive page. There's a single procedure instead of two separate procedures (such as tbh_Polls_GetActivePolls and tbh_Polls_GetArchivedPolls) because should you later want to show all polls in the management page instead of just the active one, you will just need to change a parameter when calling the procedure, instead of calling two procedures and doing additional work to merge the two resultsets.
I've already mentioned that the polls module will need a number of configuration settings that enable or disable multiple votes, make the archive public to everyone, and more. In addition, there will be settings to specify which DAL provider to use (either the supplied one for SQL Server 2005 or one for some other data store) and the connection string for the database. Following is the list of properties for a new class, named PollsElement, which inherits from the framework's ConfigurationElement class, and will read the settings of a <polls> element under the <theBeerHouse> custom configuration section (this was introduced in Chapter 3, and then used again in Chapter 5).
Property |
Description |
---|---|
ProviderType |
The full name (namespace plus class name) of the concrete provider class that implements the data access code for a specific data store |
ConnectionStringName |
The name of the entry in web.config's new <connectionStrings> section that contains the connection string to the module's database |
VotingLockInterval |
An integer indicating when the cookie with the user's vote will expire (number of days to prevent re-voting) |
VotingLockByCookie |
A Boolean value indicating whether a cookie will be used to remember the user's vote |
VotingLockByIP |
A Boolean value indicating whether the vote's IP address is kept in memory to prevent duplicate votes from that IP in the current session |
ArchiveIsPublic |
A Boolean value indicating whether the poll's archive is accessible by everyone, or if it's restricted to registered members |
EnableCaching |
A Boolean value indicating whether the caching of data is enabled |
CacheDuration |
The number of seconds for which the data is cached if there aren't inserts, deletes, or updates that invalidate the cache |
The DAL of this module is based on a simple form of the provider model design pattern, introduced in Chapter 3 and then implemented for the articles module in Chapter 5. You'll follow the same strategy here, to create a PollsProvider abstract class that defines the signature of data access methods, and some helper methods that copy data from a DataReader into a single custom entity class or a collection of objects, and then a concrete provider class, SqlPollProvider, that implements the abstract methods. The methods implemented by the SQL Server-specific provider will only be simple wrappers around the stored procedures listed above — there is a one-to-one relationship. There are also two custom entity classes, PollDetails and PollOptionDetails, which have wrapper properties for all fields of the tbh_Polls and tbh_PollOptions database tables, plus the additional Votes and Percentage fields added by the stored procedures. The diagram shown in Figure 6-2 represents all these classes and lists their methods and properties, which should be self-explanatory.
The BLL for this module is composed of a couple of classes, Poll and Option, which wrap the data of the PollDetails and PollOptionDetails, respectively, and add both instance and static methods to work with that data. These are the classes that you access from the UI layer (by means of the ObjectDataSource control, or manually) to retrieve the data to display on the page, and modify it. The UI layer must never call into the DAL directly — it only interacts with the BLL classes. The diagram in Figure 6-3 represents the business classes and their relationships.
As you see, the structure is the same as the structure employed in Chapter 5. There's a BizObject base class with a number of properties and methods that are shared among all business classes. Then there's a module-specific BasePoll class that defines the ID, AddedBy, and AddedDate properties (that both the polls and the poll options have), a reference to the PollsElement configuration element described above, and a CacheData method that caches the input data if the settings indicate that caching is active. In addition to the data read from a PollDetails object, the Poll class also has CurrentPollID and CurrentPoll static properties: the former returns the integer ID of the poll marked as current, and the latter returns an instance of Poll that completely represents the current poll.
This section describes the pages and controls that constitute the user interface layer of this module. In particular, there are two ASP.NET pages, and one user control:
~/Admin/ManagePolls.aspx: This is the page through which an administrator or editor can manage polls: add, edit, archive and remove poll options, see current results, and set the current poll. This page only lists active polls, however: Once a poll is archived, it will be visible only in the archived polls page (you can't change history).
~/ArchivedPolls.aspx: This page lists the archived polls and shows their results. If the user accessing it is an administrator or an editor, she will also see buttons for deleting polls. The archived polls are not editable; they can only be deleted if you don't want them to appear on the archive page.
The PollBox user control will enable us to insert the poll box into any page, with only a couple of lines of code. This control is central to the poll module and is described in further detail in the following section.
The ManagePoll.aspx page will be accessible only to administrator and editors, and you only have to place this page under the ~/Admin folder to protect it from unauthorized users, because that folder already has configuration settings in web.config to prevent access by anyone who is not an administrator or editor. The archive page may, or may not, be accessible to everyone, according to a custom configuration parameter, and therefore the security check will be done programmatically when the page loads.
This control has two functions:
If it detects that the user has not voted for the question yet, the control will present a list of radio buttons with the various response options, and a Vote button.
If it detects that the current user has already voted, instead of displaying the radio buttons it displays the results. It will show the percentage of votes for each option, both as a number and graphically, as a colored bar. This will also happen if the poll being shown were archived.
In both cases the control can optionally show header text and a link at the bottom that points to the archive page. This method of changing behavior based on whether the user has already voted is elegant, doesn't need an additional window, and intelligently hides the radio buttons if the user can't vote. The control's properties, which enable us to customize its appearance and behavior, are shown in the following table.
Property |
Description |
---|---|
PollID |
The ID of the poll to display in the poll box. If no ID is specified, or if it is explicitly set to -1, the poll with the IsCurrent field set to 1 will be used. |
HeaderText |
The text for the control's header bar |
ShowHeader |
Specifies whether the control's header bar is visible |
ShowQuestion |
Specifies whether the poll's question is visible |
ShowArchiveLink |
Specifies whether the control shows a link at the bottom of the control pointing to the poll's Archive page |
When you add this control to a page, you will normally configure it to show the header, the question, and the link to the archive page. If, however, you have multiple polls on the page, you may want to show the link to the archive in just one poll box, maybe the one with the poll marked as the current default. The control will also be used in the archive page itself, to show the results of the old polls (the second mode described previously): In this case the question text will be shown by some other control that lists the polls, and thus the PollBox control will have the ShowHeader, ShowQuestion, and ShowArchiveLink properties set to false.