Opinion polls consist of questions with a set of options from which users can select their response. Once a user votes in a poll, it's customary to show them current statistics about how the poll is going at that particular time. This chapter explains why polls are useful and important for different websites. Then you will learn how to design and implement a simple and maintainable voting module for the TheBeerHouse site.
There are basically two reasons why polls are used on a website: because the site's managers may be interested in what their users like (perhaps so they can modify their advertising or product offerings, or maybe in a more general sense to understand their users better) and to help users feel like they have some input to a site and are part of a community of users. Good polls always contain targeted questions that can help the site's managers learn who their users are and what they want to find on the site. This information can be used to identify which parts of the site to improve or modify. Polls are valuable for e-commerce sites, too, because they can indicate which products are of interest and in high demand. Armed with this information, e-commerce businesses can highlight those products, provide more detailed descriptions or case studies, or offer discounts to convince users to buy from their site. Another use for the information is to attract advertising revenue. Medium to large sites frequently display an "Advertise with Us" link or something similar. If you were to inquire about the possibility of advertising on a particular site, that site's advertising department would likely give you some demographics regarding the typical users of that site, such as age, the region or country they live in, common interests, and so on. This information is often gathered by direct or indirect polls. The more details you provide about your typical audience, the more chance you have of finding a sponsor to advertise on your site.
The other major benefit is user-to-user communication. Users generally like to know what their peers think about a product or a subject of interest to them. I must admit that I'm usually curious when I see a poll on a website. Even if I don't have a very clear opinion about the question being asked, I often vote just so I can see the statistics of how the other users voted! This explains why polls are usually well accepted, and why users generally vote quite willingly. Another reason why users may desire to cast a vote is that they think their opinion may influence other users or the site's managers. In addition, their votes really are important, as you've seen, and the results can definitely drive the future content of the site and perhaps even business decisions. For these reasons, you or your client may realize that you want the benefits of a poll feature, and thus you will implement some form of polling on the website.
There are some web poll design issues to consider — namely, the problems that you must address to successfully run a poll system. First of all, as with the news and other content, the same poll shouldn't remain active for too long. If you left the same poll on the page for, say, two months, you might gather some more votes, but you would lose the interest of users who voted early. If you keep a poll up for just a couple of days, you may not achieve significant results because some of your users may not have visited your site within that time frame. The right duration depends mostly on the average number of visitors you have and how often they return to your site. As a rule of thumb, if you know that several thousands of users regularly come to visit the site each week, then that is a good duration for the active poll. Otherwise, if you have fewer visitors, you can leave the poll open for two or more weeks, but probably not longer than a month.
There are several services that enable you to easily retrieve statistics such as the frequency and number of visitors and much more for your site. Some of these services are commercial, but you can find some good free ones. If you have a hosted website, you probably have access to some statistics through your hosting company's control panel, which gathers information by analyzing the IIS (Internet Information Server) log files. Of course, you could implement your own hit counter — it would be pretty easy to track visitors and generate some basic statistics — but if you want to reproduce all the advanced features offered by specialized services, it would be quite a lot of work, and it may be cheaper in the long run to subscribe to a professional service.
When you change the active poll, a new question arises: What do you do with the old questions and their results? Throw them away? Certainly not! They might be very interesting for new users who didn't take part in the vote, and the information will probably remain valid for some time, so keep them available for viewing. Old polls can be part of the useful content of your site — you could build an archive of past polls.
If you allow a user to vote as many times as he wants to, you'll end up with incorrect results. The overall results will be biased toward that user's personal opinion. Having false results is just as useless as having no results at all, because you can't base any serious decisions on them. Therefore, you want to prevent users from voting more than once for any given question. There are occasions when you might want to allow the user to vote several times, though. For example, during your own development and testing stage, you may need to post many votes to determine whether the voting module is working correctly. The administrator could just manually add some votes by entering them directly into the SQL table, but that would not tell you if the polling frontend is working right. If you enter votes using the polling user interface that you'll build in this chapter, it's more convenient and it thoroughly tests the module. There are reasons for wanting to allow multiple votes after deployment, too. Imagine that you are running a competition to select the best resource on any selected topic. The resources might be updated frequently, and if the poll lasts a month, then users may change their mind in the meantime, after voting. You may then decide to allow multiple votes, but no more than once per week (but you probably won't want to go to the trouble of letting a user eliminate his earlier vote).
discussion talks only about polls that allow a single option to be selected (poll boxes with a series of radio buttons). Another type of poll box enables users to vote for multiple options in a single step (the options are listed with checkboxes, and users can select more than one). That might be useful for a question like "What do you usually eat at pubs?" for which you want to allow multiple answers through multiple separate checkboxes. However, this type of poll is quite rare, and you could probably reword the question to ask what food they most like to eat at pubs to allow only one answer. The design of a multiple-answer poll would needlessly complicate this module, so the example here won't use that kind of functionality.
To summarize what we've discussed here: you want to implement a poll facility on the site to gauge the opinions of your users and to generate a sense of community. You don't want users to lose interest by seeing the same poll for a long time, but you do want a meaningful number of users to vote, so you'll add new questions and change the current poll often. You also want to allow users to see old polls because that helps to add useful content to your page, but they won't be allowed to vote in the old polls. Finally, you want to be able to easily add the poll to any page, and you want the results to be as unbiased and accurate as possible. Now let's look at the design in more detail, and consider how to meet these challenges.
The poll functionality for the site will store the data (questions, answers, votes, and so on) 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, an Entity Data Model, 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.
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.
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 during 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 nondefault 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 redeploying 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.
A page that displays all the archived polls and their results. A page for the results of the current poll is not necessary because they will be shown directly by the poll box — instead of the list of response options — when it detects that the user has 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 who 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.
Now let's look at designing the database tables, stored procedures, data and business layers, user interface services, and security needed for this module.
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 PollID
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 to this method is that users 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 voting 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 PollID
and user's IP address may be part of the item's key if you use the Cache
class; otherwise, the PollID
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 usernames instead of their computer's IP address. However, this only works 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 the 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. 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.
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. 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 (introduced in Chapter 3 and used again in Chapter 5).
Property | Description |
---|---|
Made obsolete with the Entity Framework. Retained from previous editions. | |
The name of the entry in | |
An integer indicating when the cookie with the user's vote will expire (number of days to prevent re-voting). | |
A Boolean value indicating whether a cookie will be used to remember the user's vote. | |
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. | |
A Boolean value indicating whether the poll's archive is accessible by everyone, or if it's restricted to registered members. | |
A Boolean value indicating whether the caching of data is enabled. | |
The number of seconds for which the data is cached if there aren't inserts, deletes, or updates that invalidate the cache. |
As in the previous chapters, a dedicated Entity Data Model (see Figure 6-2) is generated for the Poll module. It contains an entity for the Poll
and PollOption
. And as usual The EntitySet
and relationships need to be renamed to something more friendly.
The BLL for this module is composed of a series of classes, Poll
and PollOption
, which wrap the data of the tbh_Poll
and tbh_PollOption
, respectively. Both the Poll
and PollOption
have dedicated repositories that handle calling the entity model to retrieve and manipulate the data. Just as you saw in Chapter 5, there is a BasePollRepository
class that both repositories inherit common features from. The PollEntities
class is the data model's DataContext
class. Figure 6-3 illustrates the business classes and their relationships.
The PollRepository
contains the normal CRUD members, but also has a series of methods that allow it to manage the data for specific polling-related tasks. These include methods to archive and retrieve just archived polls, obtain a count of polls, get the current poll, and get its PollId
. The following table describes those methods.
The PollOptionRepository
is similar with common CRUD members and a few custom members. Custom members get the poll options for a poll and a method to register a vote. Here are the methods:
Following are the pages and controls that constitute the user interface layer of this module:
~/Admin/ManagePolls.aspx: This is the page through which an administrator or editor can view a list of polls. Icon buttons for each poll give the administrator the ability to archive, edit, or delete the poll. Before a poll is deleted, the administrator is prompted to confirm the action. Clicking the Edit button takes the administrator to the AddEditPoll.aspx
page. 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).
~/Admin/AddEditPoll.aspx: This is the page through which an administrator or editor can manage a poll: add, edit, archive, and define poll options; see current results; and set the current poll.
~/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.
For now, let's look at the PollBox
user control, which 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 was archived.
In both cases, the control can optionally show header text and a link at the bottom. The link 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 described in the following table.
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
.
Now that the design is complete, you should have a very clear idea about what is required, so now we can consider how we're going to implement this functionality. You'll follow the same order as the "Design" section, starting with the creation of database tables and stored procedures, the configuration, DAL and BLL classes, and finally the ASPX pages and the PollBox
user control.
The tables required for this module are added to the same sitewide SQL Server database shared by all modules, although the configuration settings enable you to have the data and the db
objects separated into multiple databases if you prefer to do it that way. It's easy to create the required objects with Visual Studio using the integrated Server Explorer or SQL Server Management Studio, right from within the Visual Studio IDE. Figure 6-4 is a screenshot of the IDE when adding columns to the tbh_Polls
tables, and setting the properties for the PollID
primary key column.
After creating the two tables with the columns shown in Figure 6-1, you need to create a relationship between them over the PollID
column, and set up cascade updates and deletes (Select Data
The custom configuration class must be developed before any other code because the custom settings are used in all other layers. This class is similar to the one seen in the previous chapter. It inherits from ConfigurationElement
and has the properties previously defined:
Public Class PollsElement Inherits ConfigurationElement <ConfigurationProperty("connectionStringName")> _ Public Property ConnectionStringName() As String Get Return CStr(Me("connectionStringName")) End Get Set(ByVal value As String) Me("connectionStringName") = value End Set End Property Public ReadOnly Property ConnectionString() As String Get Dim connStringName As String
If String.IsNullOrEmpty(Me.ConnectionStringName) Then connStringName = Globals.Settings.DefaultConnectionStringName Else connStringName = Me.ConnectionStringName End If Return WebConfigurationManager.ConnectionStrings(connStringName) .ConnectionString End Get End Property <ConfigurationProperty("votingLockInterval", DefaultValue:="15")> _ Public Property VotingLockInterval() As Integer Get Return CInt(Me("votingLockInterval")) End Get Set(ByVal value As Integer) Me("votingLockInterval") = value End Set End Property <ConfigurationProperty("votingLockByCookie", DefaultValue:="true")> _ Public Property VotingLockByCookie() As Boolean Get Return CBool(Me("votingLockByCookie")) End Get Set(ByVal value As Boolean) Me("votingLockByCookie") = value End Set End Property <ConfigurationProperty("votingLockByIP", DefaultValue:="true")> _ Public Property VotingLockByIP() As Boolean Get Return CBool(Me("votingLockByIP")) End Get Set(ByVal value As Boolean) Me("votingLockByIP") = value End Set End Property <ConfigurationProperty("archiveIsPublic", DefaultValue:="false")> _ Public Property ArchiveIsPublic() As Boolean Get Return CBool(Me("archiveIsPublic")) End Get Set(ByVal value As Boolean) Me("archiveIsPublic") = value End Set End Property <ConfigurationProperty("enableCaching", DefaultValue:="true")> _ Public Property EnableCaching() As Boolean Get Return CBool(Me("enableCaching")) End Get
Set(ByVal value As Boolean) Me("enableCaching") = value End Set End Property <ConfigurationProperty("cacheDuration")> _ Public Property CacheDuration() As Integer Get Dim duration As Integer = CInt(Me("cacheDuration")) If duration <= 0 Then duration = Globals.Settings.DefaultCacheDuration End If Return duration End Get Set(ByVal value As Integer) Me("cacheDuration") = value End Set End Property <ConfigurationProperty("urlIndicator")> _ Public Property URLIndicator() As String Get Dim lurlIndicator As String = Me("urlIndicator").ToString If String.IsNullOrEmpty(lurlIndicator) Then lurlIndicator = "Poll" End If Return lurlIndicator End Get Set(ByVal Value As String) Me("urlIndicator") = Value End Set End Property End Class
To make this class map a <polls>
element under the top-level <theBeerHouse>
section, we add a property of type PollsElement
to the TheBeerHouseSection
class developed in the previous chapter and then use the ConfigurationProperty
attribute to do the mapping:
<ConfigurationProperty("polls", IsRequired:=True)> _ Public ReadOnly Property Polls() As PollsElement Get Return CType(Me("polls"), PollsElement) End Get End Property
To make the archive available to everyone and disable vote locking by the user's IP, use these settings in the web.config
file:
<theBeerHouse defaultConnectionStringName="LocalSqlServer"> <contactForm mailTo="[email protected]"/> <articles pageSize="10" /> <polls archiveIsPublic="true" votingLockByIP="false" /> </theBeerHouse>
The default value will be used for all those settings not explicitly defined in the configuration file, such as connectionStringName, providerType, votingLockByCookie, votingLockInterval
, and the others.
Similar to how things were structured in the Articles module in Chapter 5 the Polls module has a set of repository classes that are located in the Polls folder of the TBHBLL class library project. There is a BasePollRepository
, which contains the common constructors, a reference to the entity model's DataContext
, and the Dispose
members. The Poll
and PollOption
entities each have a corresponding repository to manage the business logic associated for each. Since most of the patterns used in the repository members were discussed in Chapters 3 and 5, I will limit this chapter to new items.
The PollRepository
contains standard CRUD business members and some that perform targeted operations. These include archiving a poll and retrieving the current poll. All the queries are LINQ to Entities statements and cache the results when a list is retrieved according to the cache settings in the web.config
file.
Each of the entity classes generated by the Entity Data Model Wizard is a partial class that can be extended. The Polling module has two entities, Poll
and PollOption
. The main extensions that I will discuss revolve around managing the votes for a poll and each of its options.
The Poll
entity has a property that holds the number of total votes cast in a poll by calculating the SUM of the votes for each option. This is done using a LINQ statement with a LAMBDA expression.
Private _Votes As Integer = 0 Public Property Votes() As Integer Get If Me._Votes = 0 Then If PollOptions.IsLoaded = False Then Me.PollOptions.Load() End If _Votes = (From po In Me.PollOptions _ Select New With {.Votes = po.Votes}) .Sum(Function(p) p.Votes) End If Return _Votes End Get
Set(ByVal value As Integer) _Votes = value End Set End Property
The Get section of the Votes
property checks to see if the _Votes
variable is set to 0 and if it is, it tries to calculate the votes. First, it checks to see if the associated PollOptions
have been loaded in the Poll
entity; if not, they are manually loaded. This is an example of when deferred loading is not desired but easily dealt with. Now that all poll options have been loaded, a simple LINQ statement is executed calling the Sum operator and passing in the Votes
value of each poll option in a LAMBDA expression. Finally the total value is returned. This value is then used in the binding operation when the poll results are displayed.
The PollOption
entity also has a few members added to the extended partial class, a custom ToString
method, and PollId, TotalVotes
, and Percentage
properties. The ToString
method returns a custom formatted string with the PollId
, the OptionText
, and the number of votes cast for that option. This can be used as a quick method to display information about the option. PollId
is the property used to access the foreign key value of the associated Poll
. The TotalVotes
property is number of votes cast for the poll, this means a total of all the votes cast by all the poll's options. The Percentage
property returns the percentage value of the option's votes in relation to the total number of votes cast in the poll.
Public Overrides Function ToString() As String Return String.Format("{0}, {1}, {2}", Me.PollId, Me.OptionText, Me.Votes) ', {3:N1} , Me.Percentage) End Function Public Property PollId() As Integer Get If Not IsNothing(Me.PollReference.EntityKey) Then Return Me.PollReference.EntityKey.EntityKeyValues(0).Value End If Return 0 End Get Set(ByVal Value As Integer) If Not IsNothing(Me.PollReference.EntityKey) Then Me.PollReference = Nothing End If Me.PollReference.EntityKey = New EntityKey("PollEntities.Polls", "PollID", Value) End Set End Property Private _TotalVotes As Double = 0 Public Property TotalVotes() As Integer Get If Not IsNothing(Me.Poll) Then _TotalVotes = Poll.Votes End If Return _TotalVotes End Get
Set(ByVal value As Integer) _TotalVotes = value End Set End Property Public ReadOnly Property Percentage() As Double Get If TotalVotes = 0 Then Return −1D End If Return ((Votes * 100) / TotalVotes) End Get End Property
Now it's time to build the user interface: the administration page, the poll box user control, and the archive page.
The ManagePolls.aspx
page (see Figure 6-6), located under the ~/Admin folder, allows the administrator to view a list of polls, add a new poll, and edit, delete, or archive existing polls. The page is composed of the common administration layout with a menu across the top and a combination of an Accordion
and ListView
of detailed navigation on the left. It uses the Admin master page to implement the common navigation features. The table listing the polls is a ListView
with paging capabilities. The link to edit a poll is a pencil icon and the archive link is a file folder. The delete icon is the familiar trash can.
The ListView
is wrapped in an UpdatePanel
so that it can take advantage of ASP.NET AJAX without having to add any more code to the solution. The effects of the UpdatePanel
can be seen when paging through the list, or archiving or deleting a poll. These operations will occur on the server and provide seamless updates in the browser. If the poll is the current poll a checkbox icon is displayed before the action items are listed. To the left of the current icon is a tally of the total votes. The following code shows the MainContent's markup:
<asp:Content ID="MainContent" ContentPlaceHolderID="AdminContent" runat="Server"> <table cellpadding="0" cellspacing="0" class="AdminLayout"> <tr> <td> <h1> Manage Polls</h1> </td> </tr> <tr> <td> <div id="dAdminHeader"> <ul> <li><a href="ManagePolls.aspx"><span>Manage Polls</span> </a></li> <li><a href="AddEditPoll.aspx"><span>New Poll</span> </a></li> </ul> </div> </td> </tr> <tr> <td> <asp:UpdatePanel runat="server" ID="uppnlCategories"> <ContentTemplate> <asp:ListView runat="server" ID="lvPolls" DataKeyNames="PollId"> <LayoutTemplate> <table cellpadding="0" cellspacing="0" border="0"> <tr class="AdminListHeader"> <td> ID </td> <td> Poll </td> <td> Votes </td> <td> Is Current </td> <td colspan="3"> </td> </tr> <tr id="itemPlaceholder" runat="server"> </tr>
</table> </LayoutTemplate> <ItemTemplate> <tr> <td> <%#Eval("PollId")%> </td> <td> <%#Eval("QuestionText")%> </td> <td align="center"> <%#Eval("Votes")%> </td> <td align="center"> <asp:Image ID="imgIsCurrent" runat="server" ImageUrl="~/Images/OK.gif" Visible='<%# Eval("IsCurrent") %>' /> </td> <td align="center"> <a href="<%# String.Format("AddEditPoll.aspx?PollID={0}", Eval("PollId")) %>"> <img src="../images/edit.gif" alt="" width="16" height="16" class="AdminImg" /></a> </td> <td align="center"> <asp:ImageButton runat="server" ID="ibtnArchive" CommandArgument='<%# Eval("PollID").ToString() %>' CommandName="Archive" ImageUrl="~/images/folder.gif" AlternateText="Archive" CssClass="AdminImg" /> </td> <td> <asp:ImageButton runat="server" ID="btnDeleteOption" CommandArgument='<%# Eval("PollID").ToString() %>' CommandName="Delete" ImageUrl="~/images/delete.gif" AlternateText="Delete" CssClass="AdminImg" OnClientClick="return confirm('Warning: This will delete the Event from the database.')," /> </td> </tr> </ItemTemplate> </asp:ListView> <div class="pager"> <asp:DataPager ID="pagerBottom" runat="server" PageSize="15" PagedControlID="lvPolls"> <Fields> <asp:NextPreviousPagerField ButtonCssClass="command" FirstPageText="<<" PreviousPageText="<" RenderDisabledButtonsAsLabels="true" ShowFirstPageButton="true" ShowPreviousPageButton="true" ShowLastPageButton="false" ShowNextPageButton="false" /> <asp:NumericPagerField ButtonCount="7" NumericButtonCssClass="command" CurrentPageLabelCssClass="current" NextPreviousButtonCssClass="command" />
<asp:NextPreviousPagerField ButtonCssClass="command" LastPageText=">>" NextPageText=">" RenderDisabledButtonsAsLabels="true" ShowFirstPageButton="false" ShowPreviousPageButton="false" ShowLastPageButton="true" ShowNextPageButton="true" /> </Fields> </asp:DataPager> </div> </ContentTemplate> </asp:UpdatePanel> </td> </tr> </table> </asp:Content>
The ListView
itself is composed of a table that is dynamically built as the Poll
entities are bound to it. If the list of polls is less than needed to invoke paging the pager is suppressed from the bottom of the list.
Above the table are a couple of administrative links that are common in each of the Beer House module administrations. In the case of the Poll module, there are links to navigate to the ManangePolls.aspx
page and to add a new poll via the AddEditPoll.aspx page. The Edit button on each poll's record is also a hyperlink to the AddEditPoll.aspx
page, but it passes the PollID
.
Notice the JavaScript added to the Delete ImageButton
. It displays a confirmation MessageBox
to the user before it executes the delete operation. Use this technique anyplace data is being deleted or affected in a major way.
In the code-behind for the ManagePolls.aspx
page is the code to bind the polls to the ListView, Delete
, and Archive
selected polls. The Page Load
event handler checks to see if this is a postback before binding the list of polls to the ListView
. The BindPolls
method uses a PollsRepository
to get a list of polls and check whether the ListView's DataPager
should be visible:
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load If Not IsPostBack Then BindPolls() End If End Sub Private Sub BindPolls() Using Pollrpt As New PollsRepository Dim lPolls As List(Of Poll) = Pollrpt.GetPolls lvPolls.DataSource = lPolls lvPolls.DataBind() Dim pagerBottom As DataPager = lvPolls.FindControl("pagerBottom") If Not IsNothing(pagerBottom) Then
If lPolls.Count <= pagerBottom.PageSize Then pagerBottom.Visible = False Else pagerBottom.Visible = True End If End If End Using End Sub
Each poll listed in the ListView
has an archive and delete ImageButton
on the row. When these buttons are clicked the ListView
's ItemCommand
event is fired. Based on the command name associated with the ImageButton
, the appropriate action is taken. Here's the event's code:
Private Sub lvPolls_ItemCommand(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.ListViewCommandEventArgs) Handles lvPolls.ItemCommand Select Case e.CommandName Case "Delete" DeletePoll(e.CommandArgument) Case "Archive" ArchivePoll(e.CommandArgument) End Select End Sub
Both the ArchivePoll
and DeletePoll
methods use the associated methods of a PollRepository
to execute the desired action:
Private Sub ArchivePoll(ByVal pollid As Integer) Using Pollrpt As New PollsRepository Pollrpt.ArchivePoll(pollid) End Using End Sub Private Sub DeletePoll(ByVal pollId As Integer) Using Pollrpt As New PollsRepository Pollrpt.DeletePoll(pollId) End Using
Me.BindPolls() End Sub Private Sub lvPolls_ItemDeleting(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.ListViewDeleteEventArgs) Handles lvPolls.ItemDeleting DeletePoll(lvPolls.DataKeys(e.ItemIndex).Value) End Sub
The AddEditPoll.aspx
page (see Figure 6-7) provides the visual representation to manage the information about a poll, including the poll question and the associated poll options. When a new poll is being created, the poll options are suppressed until the poll question has been submitted. Once a poll exists, a list of editable poll options is displayed.
Figure 6-8 shows the page for editing an existing poll. Notice that the poll options are listed in a table on the right with the built-in capability to insert (add) a new option at the bottom of the list. Each option in the list can be edited by clicking the pencil, or deleted by clicking the trash can.
Editing the poll or a poll option can be canceled by clicking the associated Cancel hyperlink. When editing a poll is canceled, the administrator is taken to the ManagePolls.aspx
page. When a poll option is canceled, the Option textbox is cleared.
The code in the AddEditPoll.aspx.vb
code-behind file that drives the managing of a specific poll is divided into two distinct sections, one related to the poll itself and one to manage the associated poll options. The Page Load
event handler chooses either to bind the designated poll data to the corresponding controls or to clear the values for a new poll. If this is an edit operation, the BindPollOptions
method is called to bind the associated options to a ListView
. When the Update/Insert button is clicked by the administrator, the poll information is committed to the database.
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load If Not IsPostBack Then If PollId > 0 Then BindPoll() Else ClearPoll() End If End If End Sub
Private Sub BindPoll() Using Pollrpt As New PollsRepository Dim vPoll As Poll = Pollrpt.GetPollById(PollId) If Not IsNothing(vPoll) Then lblPollId.Text = vPoll.PollID lblDateAdded.Text = vPoll.AddedDate.ToShortDateString lblAddedBy.Text = vPoll.AddedBy lblDateUpdated.Text = vPoll.UpdatedDate.ToShortDateString lblUpdatedBy.Text = vPoll.UpdatedBy lblVotes.Text = vPoll.Votes txtQuestion.Text = vPoll.QuestionText cbIsCurrent.Checked = vPoll.IsCurrent BindPollOptions() lbtnInsertPoll.Text = "Update" tOptionDetail.Visible = True End If End Using End Sub Private Sub ClearPoll() lblPollId.Text = String.Empty lblDateAdded.Text = String.Empty lblAddedBy.Text = String.Empty lblDateUpdated.Text = String.Empty lblUpdatedBy.Text = String.Empty lblVotes.Text = String.Empty txtQuestion.Text = String.Empty cbIsCurrent.Checked = False lbtnInsertPoll.Text = "Insert" tOptionDetail.Visible = False End Sub Protected Sub lbtnInsertPoll_Click(ByVal sender As Object, ByVal e As EventArgs) Handles lbtnInsertPoll.Click Using Pollrpt As New PollsRepository Dim vPoll As Poll = Pollrpt.GetPollById(PollId) If IsNothing(vPoll) Then vPoll = New Poll
End If vPoll.QuestionText = txtQuestion.Text vPoll.IsCurrent = cbIsCurrent.Checked vPoll.UpdatedBy = UserName vPoll.UpdatedDate = Now If vPoll.PollID > 0 Then If Pollrpt.UpdatePoll(vPoll) Then ltlStatus.Text = "The Poll Has Been Updated." Else ltlStatus.Text = "The Poll Has Not Been Updated." End If Else vPoll.AddedBy = UserName vPoll.AddedDate = Now If Pollrpt.AddPoll(vPoll) Then ltlStatus.Text = "The Poll Has Been Added." tOptionDetail.Visible = True Else ltlStatus.Text = "The Poll Has Not Been Added." End If End If End Using End Sub
The poll options are bound to the ListView
if an existing poll is being edited. If there are no options, a message lets the user know. As soon as a new option is added it is added to the option list. This list does not contain a pager because it is more feasible to have all the poll options listed on the page.
Updating or adding a poll option works just as with any other entity; if the option exists, the OptionText
is updated and stored in the database. A new option is added to the database. The balance of the code manages deleting or selecting options from the ListView
.
Private Sub BindPollOptions() Using PollOptionRpt As New PollOptionsRepository lvPollOptions.DataSource = PollOptionRpt.GetActivePollOptionsByPollId( PollId) lvPollOptions.DataBind() End Using End Sub Protected Sub lbInsert_Click(ByVal sender As Object, ByVal e As EventArgs) Handles lbInsert.Click UpdatePollOptions()
End Sub Private Sub UpdatePollOptions() Using PollOptionsrpt As New PollOptionsRepository Dim lPollOption As PollOption If PollOptionId > 0 Then lPollOption = PollOptionsrpt.GetPollOptionById(PollOptionId) Else lPollOption = New PollOption() End If lPollOption.PollId = PollId lPollOption.OptionText = txtOption.Text lPollOption.UpdatedDate = Now lPollOption.UpdatedBy = UserName If lPollOption.OptionID > 0 Then If PollOptionsrpt.UpdatePollOption(lPollOption) Then IndicateOptionUpdated() Else IndicateOptionNotUpdated(PollOptionsrpt) End If Else lPollOption.Active = True lPollOption.AddedBy = UserName lPollOption.AddedDate = Now If PollOptionsrpt.AddPollOption(lPollOption) Then IndicateOptionUpdated() Else IndicateOptionNotUpdated(PollOptionsrpt) End If End If lbInsert.Text = "Insert" End Using End Sub Private Sub IndicateOptionNotUpdated(ByVal vRepository As BaseRepository) ltlStatus.Text = String.Empty If vRepository.ActiveExceptions.Count > 0 Then For Each kv As KeyValuePair(Of String, Exception) In vRepository.ActiveExceptions ltlStatus.Text += DirectCast(kv.Value, Exception).Message & "<BR/>" Next Else
ltlStatus.Text = "The Option Has Not Been Updated." End If End Sub Private Sub IndicateOptionUpdated() ltlStatus.Text = "The Option Has Been Updated." ' cmdDelete.Visible = True txtOption.Text = String.Empty Me.BindPollOptions() End Sub Private Sub DeletePollOption(ByVal OptionId As Integer) Using PollOptionsrpt As New PollOptionsRepository PollOptionsrpt.DeletePollOption(PollOptionsrpt .GetPollOptionById(OptionId)) End Using Me.BindPollOptions() End Sub Private Sub lvPollOptions_ItemDeleting(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.ListViewDeleteEventArgs) Handles lvPollOptions.ItemDeleting DeletePollOption(lvPollOptions.DataKeys(e.ItemIndex).Value) End Sub Private Sub lvPollOptions_ItemEditing(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.ListViewEditEventArgs) Handles lvPollOptions.ItemEditing PollOptionId = lvPollOptions.DataKeys(e.NewEditIndex).Value Using lPollOptionrpt As New PollOptionsRepository Dim lPollOption As PollOption = lPollOptionrpt.GetPollOptionById(PollOptionId) txtOption.Text = lPollOption.OptionText lbInsert.Text = "Update" End Using End Sub
You'll plug the PollBox
user control into the site's common layout (the master page). The PollBox.ascx
user control is created under the ~/Controls
folder, together with all other user controls.
This user control can be divided into four parts. The first defines a panel with an image and a label for the configurable header text. This content is placed into a Panel
so that it can be hidden if the ShowHeader
property is set to false
. It also defines another label for the poll's question text. Here's the code:
<%@ Control Language="C#" AutoEventWireup="true" CodeFile="PollBox.ascx.cs" Inherits="PollBox" %> <div class="pollbox">
<asp:Panel runat="server" ID="panHeader"> <div class="sectiontitle"> <asp:Image ID="imgArrow" runat="server" ImageUrl="~/images/arrowr.gif" style="float: left; margin-left: 3px; margin-right: 3px;"/> <asp:Label runat="server" ID="lblHeader"></asp:Label> </div> </asp:Panel> <div class="pollcontent"> <asp:Label runat="server" ID="lblQuestion" CssClass="pollquestion"></asp:Label>
The second part is a Panel
to show when the poll box allows the user to vote (i.e., when it detects that the poll being shown is not archived, and the user has not already voted for it). The Panel
contains a RadioButtonList
to list the options, a RequiredFieldValidator
that ensures that at least one option is selected when the form is submitted, and the button to do the postback:
<asp:Panel runat="server" ID="panVote"> <div class="polloptions"> <asp:RadioButtonList runat="server" ID="optlOptions" DataTextField="OptionText" DataValueField="ID" /> <asp:RequiredFieldValidator ID="valRequireOption" runat="server" ControlToValidate="optlOptions" SetFocusOnError="true" Text="You must select an option." ToolTip="You must select an option" Display="Dynamic" ValidationGroup="PollVote"></asp:RequiredFieldValidator> </div> <asp:Button runat="server" ID="btnVote" ValidationGroup="PollVote" Text="Vote" OnClick="btnVote_Click" /> </asp:Panel>
The third part defines the Panel
to be displayed when the control detects that the user has already voted for the current poll. In this situation, the control displays the results, which is done by means of a Repeater
that outputs the option text and the number of votes it has received. It also creates a <div>
element whose width
style attribute is set to the option's Percentage
value, so that the user will get a visual representation of the vote percentage, in addition to seeing the percentage as a number:
<asp:Panel runat="server" ID="panResults"> <div class="polloptions"> <asp:ListView runat="server" ID="lvOptions"> <LayoutTemplate> <div runat="server" id="itemPlaceHolder"> </div> </LayoutTemplate> <ItemSeparatorTemplate> <img runat="server" src="~/Images/spacer.gif" height="5" meta:resourcekey="imgSeparatorResource1" alt="" /><br /> </ItemSeparatorTemplate> <ItemTemplate> <div> <%# Eval("OptionText") %> <small>(<%# Eval("Votes") %> vote(s) - <%# GetFixedPercentage(Eval("Votes"), TotalVotes) %>
%)</small> <br /> <div class="pollbar" style="width: <%# GetFixedPercentage(Eval("Votes"), TotalVotes) %>%"> </div> </div> </ItemTemplate> </asp:ListView> <br /> <b>Total votes: <asp:Label runat="server" ID="lblTotalVotes" /></b> </div> </asp:Panel>
Finally, the last section of the control defines a link to the archive page, which can be hidden by means of the control's ShowArchiveLink
custom property, plus a couple of closing tags for <div>
elements opened earlier to associate some CSS styles to the various parts of the control:
<asp:HyperLink runat="server" ID="lnkArchive" NavigateUrl="~/ArchivedPolls.aspx" Text="Archived Polls" /> </div> </div>
The PollBox.ascx
control's code-behind file begins by defining all those custom properties described in the "Design" section. Most of these properties are just wrappers for the Text
or Visible
properties of inner labels and panels, so they don't need their values persisted:
Partial Public Class PollBox Inherits System.Web.UI.UserControl #Region " Property " Private _pollID As Integer = −1 <Personalizable(PersonalizationScope.Shared), _ WebBrowsable(), _ WebDisplayName("Show Archive Link"), _ WebDescription("Specifies whether the link to the archive page is displayed")> _ Public Property ShowArchiveLink() As Boolean Get Return lnkArchive.Visible End Get Set(ByVal value As Boolean) lnkArchive.Visible = value End Set End Property Public Property ShowQuestion() As Boolean Get Return lblQuestion.Visible End Get Set(ByVal value As Boolean) lblQuestion.Visible = value End Set
End Property Public Shared ReadOnly Settings As TheBeerHouseSection = Helpers.Settings #End Region
The PollID
property does not wrap any other property, and therefore its value is manually stored in and retrieved from the control's state, as part of the control's ViewState
collection. As already shown in previous chapters, this is done by overriding the control's LoadControlState
and SaveControlState
methods and registering the control to specify that it requires the control state, from inside the Init
event handler:
<Personalizable(PersonalizationScope.Shared), _ WebBrowsable(), _ WebDisplayName("Poll ID"), _ WebDescription("The ID of the poll to show")> _ Public Property PollID() As Integer Get Return _pollID End Get Set(ByVal value As Integer) _pollID = value End Set End Property Protected Sub Page_Init(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Init Me.Page.RegisterRequiresControlState(Me) End Sub Protected Overrides Sub LoadControlState(ByVal savedState As Object) Dim ctlState() As Object = CType(savedState, Object()) MyBase.LoadControlState(ctlState(0)) Me.PollID = CInt(ctlState(1)) End Sub Protected Overrides Function SaveControlState() As Object Dim ctlState() As Object ReDim ctlState(2) ctlState(0) = MyBase.SaveControlState() ctlState(1) = Me.PollID Return ctlState End Function
The control can be shown because it is explicitly defined on the page, or because it is dynamically created by some template-based control, such as ListView, Repeater, DataList, DataGrid, GridView
, and DetailsView
. In the first case, the code that loads and shows the response options (in either edit or display mode) will be run from the control's Load
event handler. Otherwise, it will run from the control's DataBind
method, which you can override. The code itself is placed in a separate method, DoBinding
, and it's called from these two methods, as follows:
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
If Not Me.IsPostBack Then DoBinding() End Sub Public Overrides Sub DataBind() ' the call to the base DataBind makes a call to OnDataBinding, ' which parses and evaluates the control's binding expressions, i.e. the PollID prop MyBase.DataBind() ' with the PollID set, do the actual binding DoBinding() End Sub
Note that in the DataBind
method, the base version of DataBind
is called before executing the custom binding code of DoBinding
. The call to the base version, in turn, makes a call to the control's standard OnDataBinding
method, which parses and evaluates the control's expressions. This is necessary because when the control is placed into a template, it will have the PollID
property bound to some expression, and this binding expression must be evaluated before actually executing the DoBinding
method, so that it will find the final PollID
value.
The DoBinding
method retrieves the data from the database (via the BLL), binds it to the proper RadioButtonList
and the Repeater
controls, and either shows or hides the options or results, depending on whether the user has already voted for the question being asked. However, before retrieving the poll and its options, it must check whether the PollID
property is set to −1
, in which case it must first retrieve the ID of the current poll:
Public Property TotalVotes() As Integer Get If Not IsNothing(ViewState("TotalVotes")) AndAlso IsNumeric(ViewState("TotalVotes")) Then Return CInt(ViewState("TotalVotes")) End If Return 0 End Get Set(ByVal Value As Integer) ViewState("TotalVotes") = Value End Set End Property Protected Sub DoBinding() panResults.Visible = False panVote.Visible = False Using Pollrpty As New PollsRepository Dim lpollID As Integer = If(Me.PollID = −1, Pollrpty.CurrentPollID, Me.PollID) If lpollID > −1 Then Dim lpoll As Poll = Pollrpty.GetPollById(lpollID, False) If Not IsNothing(lpoll) Then lblQuestion.Text = lpoll.QuestionText
TotalVotes = lpoll.Votes.ToString lblTotalVotes.Text = TotalVotes valRequireOption.ValidationGroup &= lpoll.PollID.ToString btnVote.ValidationGroup = valRequireOption.ValidationGroup If lpoll.IsArchived Or GetUserVote(lpollID) > 0 Then lvOptions.DataSource = lpoll.PollOptions lvOptions.DataBind() panResults.Visible = True Else optlOptions.DataSource = lpoll.PollOptions optlOptions.DataBind() panVote.Visible = True End If End If End If End Using End Sub
To check whether the current user has already voted for the poll, a call to the GetUserVote
method is made. If the method returns a value greater than 0
, it means that a vote for the specified poll was found. You'll see the code for this method in a moment, but first consider the code executed when the Vote button is clicked. The button's Click
event handler calls the Poll.VoteOption
business method to add a vote for the specified option (whose ID is read from the RadioButtonList'
s SelectedValue
), and then shows the results panel and hides the edit panel. In order to remember that the user has voted for this poll, you create a cookie named Vote_Poll{x}
, where {x}
is the ID of the poll. The cookie's value is the ID of the option the user has voted for. The cookie is created only if the VotingLockByCookie
configuration property is set to true
(the default) and the cookie's expiration is set to the current date plus the number of days also stored in the <polls>
custom configuration element (15
by default). Finally, it saves the votes in the cache (unless the VotingLockByIP
setting is set to false
), to ensure that it will be remembered at least for the current user's session even if the client has his cookies turned off. The cache's key is defined as {y}_Vote_Poll{x}
, where {y}
is replaced by the client's IP address. This is necessary because the Cache
is not user-specific like session state, and thus you need to create different keys for different users. Here's the code of the Click
event handler:
Protected Sub btnVote_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnVote.Click Using Pollrpty As New PollsRepository Dim lpollID As Integer = If(Me.PollID = −1, Pollrpty.CurrentPollID, Me.PollID) ' check that the user has not already voted for this poll Dim userVote As Integer = GetUserVote(lpollID) If userVote = 0 Then ' post the vote and then create a cookie to remember this user's vote userVote = Convert.ToInt32(optlOptions.SelectedValue) Using PollOptionRptry As New PollOptionsRepository
PollOptionRptry.Vote(userVote) End Using ' hide the panel with the radio buttons, and show the results DoBinding() panVote.Visible = False panResults.Visible = True Dim expireDate As DateTime = DateTime.Now.AddDays( _ Settings.Polls.VotingLockInterval) Dim key As String = "Vote_Poll" & lpollID.ToString ' save the result to the cookie If Settings.Polls.VotingLockByCookie Then Dim cookie As New HttpCookie(key, userVote.ToString) cookie.Expires = expireDate Me.Response.Cookies.Add(cookie) End If ' save the vote also to the cache If Settings.Polls.VotingLockByIP Then Cache.Insert( _ Me.Request.UserHostAddress.ToString & "_" & key, _ userVote) End If End If End Using End Sub
The final piece of code is the GetUserVote
method discussed earlier, which takes the ID of a poll, and checks whether it finds a vote in a client's cookie or in the cache, according to the VotingLockByCookie
and VotingLockByIP
settings, respectively. If no vote is found in either place, 0
is returned, indicating that the current user has not yet voted for the specified poll:
Protected Function GetUserVote(ByVal vpollID As Integer) As Integer Dim key As String = "Vote_Poll" & vpollID.ToString Dim key2 As String = Me.Request.UserHostAddress.ToString & "_" & key ' check if the vote is in the cache If Settings.Polls.VotingLockByIP And Not IsNothing(Cache(key2)) Then Return CInt(Cache(key2)) End If ' if the vote is not in cache, check if there's a client-side cookie If Settings.Polls.VotingLockByCookie Then Dim cookie As HttpCookie = Me.Request.Cookies(key) If Not IsNothing(cookie) Then Return Integer.Parse(cookie.Value) End If End If Return 0 End Function
Protected Function GetFixedPercentage(ByVal vVotes As Integer, ByVal vTotalVotes As Integer) As Integer Dim val As Double = (vVotes * 100) / If(vTotalVotes > 0, vTotalVotes, 1) Dim percentage As Integer = Convert.ToInt32(val) Select Case val Case 100 percentage = 98 Case −1 percentage = 0 End Select Return percentage End Function
The PollBox
user control is now ready, and you can finally plug it into any page. For this sample site we'll put it into the site's master page, so that the polls will be visible in all pages. As an example of adding more, you can add two PollBox
instances to the master page: the first will have no PollID
specified, so that it will dynamically use the current poll, and the second one has the PollID
property set to a specific value so that it can reference a different poll and has the ShowArchiveLink
property set to false
to hide the link to the archive page, as it's already shown by the first poll box. Here's the code:
<%@ Register Src="Controls/PollBox.ascx" TagName="PollBox" TagPrefix="mb" %> ... <mb:PollBox id="PollBox1" runat="server" HeaderText="Poll of the week" /> <mb:PollBox id="PollBox2" runat="server" HeaderText="More polls" PollID="18" ShowArchiveLink="False" />
Figure 6-9 shows the result: the home page with the two poll boxes displayed in the site's left-hand column. You can change the first poll simply by going to the administrative page and setting a different (existing or new) poll as the current one. If you want to change the second, you'll need to change the ID in the master page's source code file.
ArchivedPolls.aspx
is the last page to develop for this module. It lists all archived polls, one per line, and when the user clicks one it has to expand and display its options and results. The page allows you to have multiple questions expanded at the same time if you prefer. It initially shows them in "collapsed" mode because you don't want to create a very long page, distracting users and making it hard for them to search for a particular question. Displaying the questions only when the page is first loaded produces a cleaner and more easily navigable page. If the current user is an administrator or an editor, she will also see command links on the right side of the listed polls to delete them. Figure 6-10 shows the page as seen by a normal anonymous user, with two of the three polls on the page expanded.
If you compare the poll results on the page's central section with the results of the poll in the left column, you'll notice that they look similar. Actually, they are nearly identical, except for the fact that in the former case the question text is shown as a link and not as bold text. As you can easily guess, the poll results rendered in the page's content section are created by PollBox
controls, which have the ShowHeader, ShowQuestion
, and ShowArchiveLink
properties set to false
. The link with the poll's text is created by a binding expression defined within the ItemTemplate
of a parent ListView
control. The PollBox
control itself is defined inside the sample template section and has its PollID
property set to a binding expression that retrieves the PollID
value from the Poll
object being bound to every row, and is wrapped by a <div>
that is hidden by default (it has the display
style attribute set to none
). When the user clicks the link, he doesn't navigate to another page, but executes a local JavaScript function that takes the name of a <div>
(named after poll{x}
, where {x}
is the ID of a poll) and toggles its display state (if set to none, it sets it to an empty string to make it visible, and vice versa). Following is the code for the JavaScript function, located in the TBH.js
file, which hides and shows the <div>
with the results:
function toggleDivState(divName) { var ctl = window.document.getElementById(divName); if (ctl.style.display == "none") ctl.style.display = ""; else ctl.style.display = "none"; }
Here is the HTML markup containing the ListView
definition and other page information:
<div class="sectiontitle">Archived Polls</div> <p>Here is the complete list of archived polls run in the past. Click on the poll's question to see its results.</p> <asp:ListView ID="lvPolls" runat="server" ItemPlaceholderID="itemPlaceHolder"> <LayoutTemplate> <div runat="server" id="itemPlaceHolder" /> </LayoutTemplate> <ItemTemplate> <div> <img src="Images/ArrowR2.gif" /> <a href="javascript:toggleDivState('poll<%# Eval("PollID") %>'),"> <%# Eval("QuestionText") %></a> <small>(archived on <%# Eval("ArchivedDate", "{0:d}") %>)</small> <div style="display: none;" id="poll<%# Eval("PollID") %>"> <uc1:PollBox ID="PollBox1" runat="server" PollID=' <%# Eval("PollID") %>' ShowHeader="False" ShowQuestion="False" ShowArchiveLink="False" /> </div> <asp:ImageButton runat="server" ID="ibtnDelete" ImageUrl="~/Images/Delete.gif" CommandName ="Delete" /> </div> </ItemTemplate> <EmptyDataTemplate> <div> <b>No polls to show</b></div> </EmptyDataTemplate> </asp:ListView>
In the ListView
just defined, an ImageButton
is declared to create a Delete
command for each of the listed polls. However, this command must be visible only to users who belong to the Editors and Administrators roles. When the page loads, if the user is not authorized to delete polls, then the delete ImageButton
is hidden. Before doing this, however, you have to check whether the user is anonymous and, if so, whether the page is accessible to everyone or only to registered members. If the check fails, the RequestLogin
method of the BasePage
base class is called to redirect the user to the login page. Here's the code:
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load If Not Me.User.Identity.IsAuthenticated AndAlso Not Settings.Polls.ArchiveIsPublic Then Me.RequestLogin() End If If Not IsPostBack Then BindPolls() End If End Sub
The list of archived polls is bound to the ListView
by calling the GetArchivedPolls
method of the PollRepository
in a using
statement.
Private Sub BindPolls() Using lPollrpty As New PollsRepository lvPolls.DataSource = lPollrpty.GetArchivedPolls lvPolls.DataBind() End Using End Sub
The only other code in the code-behind file for this page is the event handler for the ListView'
s ItemCreated
event, from which you add the JavaScript confirmation pop-up to the Delete
command buttons:
Private Sub lvPolls_ItemCreated(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.ListViewItemEventArgs) Handles lvPolls.ItemCreated If e.Item.ItemType = ListViewItemType.DataItem Then Dim ibtnDelete As ImageButton = CType(e.Item .FindControl("ibtnDelete"), ImageButton) ibtnDelete.Visible = Me.User.Identity.IsAuthenticated And _ (Me.User.IsInRole("Administrators") Or Me.User.IsInRole("Editors")) ibtnDelete.OnClientClick = "if (confirm('Are you sure you want to delete this poll?') == false) return false;" End If End Sub
This chapter presents a working solution for handling multiple dynamic polls on your website. The complete polls module is made up of an administration console for managing the polls through a web browser, integration with the membership system to secure the administration and archive pages, and a user control that enables us to show different polls on any page using only a couple of lines of code. This module can easily be employed in many real-world sites as it is now, but of course you can expand and enhance it as desired. Here are a few suggestions:
Add the capability to remind users which option they voted for. Currently, they can see the results, but the control does not indicate how they voted; the vote is stored in a cookie, which is easy to retrieve.
Add a ReleaseDate
and an ExpireDate
to the polls, so that you can schedule the current poll to change automatically. We do this type of thing with the articles module.
Provide the option to allow only registered users to vote.
Expand the capabilities of the Poll module to create a survey by chaining questions together.
Add the ability to let users register once they have completed a poll or survey for contest prizes.
Allow voting to be multiple choice for selected questions.
In the next chapter, you continue the development of the TheBeerHouse site through the addition of another module that integrates with the rest of the site's architecture. The new module will be used for creating and sending out newsletters to users who subscribed to the newsletter at registration time.