Chapter 6. Opinion Polls

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.

Problem

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.

Design

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.

Handling Multiple Votes

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.

Designing the Database Tables

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.

Figure 6-1

Figure 6.1. Figure 6-1

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.

Designing the Configuration Module

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

ProviderType

Made obsolete with the Entity Framework. Retained from previous editions.

ConnectionStringName

The name of the entry in web.config's new <connectionStrings> section, which 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.

Creating the Entity Data Model

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.

Figure 6-2

Figure 6.2. Figure 6-2

Designing the Business Layer

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.

Figure 6-3

Figure 6.3. Figure 6-3

The PollRepository

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.

Method

Description

GetPolls

Retrieves a full list of polls.

GetArchivedPolls

Retrieves a list of archived polls.

GetPollById

Retrieves a poll by its PollId.

GetPollCount

Returns the number of polls in the database.

AddPoll

Adds a new poll to the database.

UpdatePoll

Updates an existing poll.

DeletePoll

Deletes a poll by setting the Active flag to false.

UnDeletePoll

Undeletes a poll by setting the Active flag to true.

ArchivePoll

Archives the designated poll by setting the CurrentPoll value to 0 and IsArchived to 1.

GetCurrentPollId

Returns the PollId of the poll designated as the currently active poll.

CurrentPoll

Retrieves an instance of the current Poll.

The PollOptionRepository

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:

Method

Description

GetPollOptions

Retrieves a full list of poll options.

GetActivePollOptionsByPollId

Retrieves a list of active poll options by the specified PollId.

GetPollOptionsByPollId

Retrieves a list of poll options by the specified PollId.

GetPollOptionById

Retrieves the PollOption by the specified PollOptionId.

GetPollOptionCount

Returns a count of poll options.

AddPollOption

Adds a new poll option to the database.

UpdatePollOption

Updates an existing poll option.

DeletePollOption

Deletes a poll option by setting the Active flag to false.

UnDeletePollOption

Undeletes a poll by setting the Active flag to true.

Vote

Returns the PollId of the poll designated as the currently active poll.

Designing the User Interface Services

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:

  1. 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.

  2. 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.

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.

Solution

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.

Working on the Database

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.

Figure 6-4

Figure 6.4. Figure 6-4

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

Figure 6-4
Figure 6-5

Figure 6.5. Figure 6-5

Implementing the Configuration Module

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.

Implementing the Repositories

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.

Implementing the PollRepository

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.

Implementing the PollOptionRepository

Like the PollRepository, the PollOptionRepository contains common CRUD members and a member to cast a vote for an option and another member to get options by a PollId.

Extending the Entity Model Entities

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.

Extending the Poll Entity

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.

Extending the PollOption Entity

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

Implementing the User Interface

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

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.

Figure 6-6

Figure 6.6. Figure 6-6

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.

The ManagePolls.aspx.vb Code-Behind File

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

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-7

Figure 6.7. Figure 6-7

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.

Figure 6-8

Figure 6.8. Figure 6-8

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 AddEditPoll.aspx.vb Code-Behind File

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

The PollBox.ascx User Control

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) %>%">&nbsp;</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.vb Code-Behind File

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

Plugging the PollBox Control into the Site's Layout

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.

Figure 6-9

Figure 6.9. Figure 6-9

The ArchivedPolls.aspx Page

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.

Figure 6-10

Figure 6.10. Figure 6-10

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>

The ArchivedPolls.aspx.vb Code-Behind File

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

Summary

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset