Chapter 5. News and Article Management

The example site is basically a container for content targeted to beer, food, and pub enthusiasts. Content can be in the form of news, articles, reports of special events, reviews, photo galleries, and so forth. This chapter describes the typical content-related problems that should be considered for a site of this type. You'll then design and develop an online article manager that allows the complete management of the site's content, in terms of acquiring articles; adding, activating, and removing articles; sharing articles with other parties, and so on.

Problem

Different sites use different methods of gathering news and information: some site administrators hunt for news events and write their own articles, while others get news and articles directly from their users (a great example of this is the Add Your News link at www.aspwire.com) or they rely upon a company whose business is to gather and organize news to be sold to third-party sites. In the old days, some sites did screen-scraping, retrieving data from an external site's page and showing it on their pages with a custom appearance (of course, you must have the authorization from the external company and you must know the format they use to show the news on their pages). During the last few years, we've seen an explosion in the use of RSS (Really Simple Syndication), a simple XML format for syndicating content, making it available to other clients. Atom is another XML-based syndication standard that was created to solve some problems of RSS — it is relatively new but already very popular. The basic idea with RSS and ATOM is for sites to provide an index of news items in the form of an XML document. A client program can fetch that XML document and provide users with a list of news items and hyperlinks that can direct them to the individual stories they are interested in. One site's XML index document is called a newsfeed. The client program is called a news aggregator (or feed reader) because it can extract newsfeeds from many sites and present them in one list, possibly arranged by categories. Users can subscribe to the XML feed and their aggregator program can periodically poll for new stories by fetching new XML documents automatically in the background. Because RSS and Atom are open standards, there are many web-based and fat-client desktop applications that can subscribe to any site that provides such feeds. Some popular open-source feed readers written in C# are RSS Bandit (www.rssbandit.org) and SharpReader (www.sharpreader.com). RSS and Atom are very convenient for users who want to keep up on the latest news and articles. You can advertise your new content via RSS and ATOM feeds, or you can even display a list of content from other sites by showing RSS links on one of your web pages. Your page can have an aggregator user control that makes it simple to display the content of specified RSS and ATOM feeds. This adds to any unique content you provide, and users will find value in returning to your site frequently to see your own updated content as well as a list of interesting links to updated news items on other sites.

It doesn't matter which methods you decide to use, but you must have fresh and updated content as often as possible for your site to be successful and entice users to return. Users will not return regularly to a site if they rarely find new content. You should use a variety of methods to acquire new content. You can't rely entirely on external content (retrieved as an RSS feed, by screen-scraping, or by inserting some JavaScript) because these methods often imply that you just publish a small extract of the external content on your site, and publish a link to the full article, thus driving traffic away from your site. It can be a solution for daily news about weather, stock exchanges, and the like, but not for providing original content, which is why users surf the web. You must create and publish some content on your own, and possibly syndicate that content as RSS feeds, so that other sites can consume it, and bring new visitors to your site.

Once you have a source of articles, a second problem arises: how do you add them to your site? You can immediately rule out manually updating pages or adding new static HTML pages — if you have to add news several times a day, or even just every week, creating and uploading pages and editing all the links becomes an administrative nightmare. Additionally, the people who administer the site on a daily basis may not have the skills required to edit or create new HTML pages. You need a much more flexible system, one that allows the site administrators to easily publish fresh content without requiring special HTML code generation tools or knowledge of HTML. You want it to have many features, such as the capability to organize articles in categories and show abstracts, and even to allow some site users to post their own news items. You'll see the complete list of features you're going to implement in the "Design" section of this chapter. For now, suffice it to say that you must be able to manage the content of your site remotely over the web, without requiring any other tools. Think about what this implies: you can add or edit news as soon as it is available, in a few minutes, even if you're not in your office and even if you don't have access to your own computer; all you need is a connection to the Internet and a browser. And this can work the same way for your news contributors and partners. They won't have to e-mail the news to you and then wait for you to publish it. They can submit and publish content without your intervention (although in our case we will give administrators and editors the option to approve or edit the content before publication).

The last problem is the implementation of security. We want to give full control to one or more administrators and editors, allow a specific group of users (contributors) to submit news, and allow normal users to just read the news. You could even prevent them from reading the content if they have not registered with the site.

Once new articles or news is made available, you need to promote the content. In today's Web 2.0 world, this involves pushing content to social media sites such as Twitter, Facebook, and many other locations. The other side of Web 2.0 design is allowing visitors to comment on the content. The Beer House already allows comments, but the user interface needs a slight facelift.

To summarize the problem, you need the following:

  • An online tool for managing news content that allows specific users to add, update, and delete articles without knowing HTML or other publishing software.

  • A common infrastructure to post announcements to Twitter. This will provide a way to get news to patrons through the web and SMS (Short Message Service). Twitter also allows the Beer House to reach a growing Web 2.0 crowd of very interactive customers.

  • A common infrastructure to leverage Gravatar.com to create user avatars used to visually identify them on the site when they post a comment to an article.

  • A method of allowing other sites to use your content so that they publish an extract and link to your site for the entire articles, thus bringing more traffic.

  • A system that allows various users different levels of access to the site's content.

Design

This section introduces the design of the solution and an online tool for acquiring, managing, and sharing the content of our site. Specifically, we will do the following:

  • Provide a full list of the features we want to implement.

  • Design the database tables for this module.

  • Create a list and a description of the stored procedures needed to provide access to the database.

  • Design the object models of the data and business layers.

  • Describe the user interface services needed for content management, such as the site pages and reusable user controls.

  • Explain how we will ensure security for the administration section and for other access-restricted pages.

Features to Implement

Let's start our discussion by writing down a partial list of the features that the article manager module should provide to be flexible and powerful, but still easy to use:

  • An article can be added to the database at any time, with an option to delay publication until a specified release date. Additionally, the person submitting the article must be able to specify an expiration date, after which the article will be automatically retired. If these dates are not specified, then the article should be immediately published and remain active indefinitely.

  • Articles can have an approved status. If an administrator or editor submits the article, it should be approved immediately. If you allow other people, such as staff or users of the site (we will call them contributors), to post their own news and articles, then this content should be added to the database in a "pending" state. The site administrators or editors will then be able to control this content, apply any required modifications, and finally approve the articles for publishing once they are ready.

  • The system must also track who originally submitted an article or news item. This is important because it provides information regarding whether a contributor is active, who is responsible for incorrect content, who to contact for further details if the article is particularly interesting, and so on.

  • The administrator/editor must be able to decide whether an article can be read by all readers or only by registered users.

  • There can be multiple categories, enabling articles to be organized in different virtual folders. Each category should have a description and an image that graphically represents it.

  • There should be a page with the available categories as a menu. Each category should be linked to a page that shows a short abstract for each published article. Clicking on the article's title should allow the user to read the whole text.

  • Articles can be targeted to users from a specified location, for example, country, state/province, or city. Consider the case where you might have stories about concerts, parties, and special events that will happen in a particular location. In Chapter 4, you implemented a registration and profiling system that includes the user's address. That will be used here to highlight events that are going to happen close to the user's location. This is a feature that can entice readers to provide that personal information, which you could use later for marketing purposes (ads can be geographically targeted also).

  • Users can leave comments or ask questions about articles, and this feedback should be published at the end of the article itself, so that other readers can read it and create discussions around it (this greatly helps to increase traffic). You might recognize this approach as being common to blogs, which are web logs in which an individual publishes personal thoughts and opinions and other people add comments. As another form of feedback, users can rate articles to express how much they liked them.

  • Each comment needs to be screened for spam. Letting comment spam infiltrate the site can quickly diminish the desirability of the site to users. It could also degrade the quality of the site in the eyes of search engines, meaning fewer potential customers. Automatic checking and manual approval must be implemented.

  • The module must count how many times an article is read. This information will also be shown to the reader, together with the abstract, the author name, the publication date, and other information. But it will be most important for the editors/administrators because it greatly helps them understand which topics the readers find most interesting, enabling administrators to direct energy, money, and time to adding new content on those topics.

  • The new content must be available as an RSS feed to which a reader can subscribe to read through his or her favorite RSS aggregator.

  • Above all, the article manager and the viewer must be integrated with the existing site. In our case, this means that the pages must tie in with the current layout and that we must take advantage of the current authentication/authorization system to protect each section and to identify the author of the submitted content.

It's essential to have this list of features when designing the database tables, as we now know what information we need to store, and the information that we should retrieve from existing tables and modules (such as the user account data).

Designing the Database Tables

As described in Chapter 3 (where we looked at building the foundations for our site), we're going to use the tbh_ prefix for all our tables, so that we avoid the risk of naming a table such that it clashes with another table used by another part of the site (this may well be the case when you have multiple applications on the site that store their data on the same shared DB). We need three tables for this module: one for the categories, another one for the articles, and the last one for the user feedback. The diagram shown in Figure 5-1 illustrates how they are linked to each other.

Figure 5-1

Figure 5.1. Figure 5-1

Let's start by looking at these tables and their relationship in more detail.

The tbh_Categories Table

Unsurprisingly, the tbh_Categories table stores some information about the article categories:

Column Name

Type

Size

Allow Null

Description

CategoryID

int - PK

4

No

Unique ID for the category.

AddedDate

datetime

8

No

Category creation date/time.

AddedBy

nvarchar

256

No

Name of the user who created the category.

Title

nvarchar

256

No

Category's title.

Importance

int

4

No

Category's importance. Used to sort the categories with a custom order, other than by name or by date.

Description

nvarchar

4000

Yes

Category's description.

ImageUrl

nvarchar

256

Yes

URL of an image that represents the category graphically.

Active

Bit

1

No

Indicates if the record has been deleted or is active.

UpdatedDate

datetime

8

No

Category last update date/time.

UpdatedBy

nvarchar

256

No

Name of the user who last updated the category.

This system supports a single-level category, meaning that we cannot have subcategories. This is plenty for small-to-midsized sites that don't have huge numbers of new articles on a wide variety of topics. Having too many categories in sites of this size can even hinder the user's experience, because it makes it more difficult to locate desired content. Enhancing the system to support subcategories is left as an exercise if you really need it, but as a suggestion, the DB would only require an additional ParentCategoryID column containing the ID of the parent category.

AddedDate, AddedBy, UpdatedDate, and UpdatedBy are four columns that you will find in all our tables — they record when a category/article/comment/product/message/newsletter was created and last updated, and by whom, to provide an audit trail. Of course, this will only provide information about when the record was created and last modified. For a more robust auditing system, you might look at adding a general table to log all changes to the database, but again this is a bit more than the Beer Houses needs at this point. You may have thought that, instead of having an nvarchar column for storing the username, we could use an integer column that would contain a foreign key pointing to records of the aspnet_Users table introduced in Chapter 4. However, that would be a bad choice for a couple of reasons:

  1. The membership data may be stored in a separate database, and possibly on a different server.

  2. The membership module might use a provider other than the default one that targets SQL Server. In some cases, the user account data will be stored in Active Directory or maybe an Oracle database, and thus there would be no SQL Server table to link to.

Another column that has been added to all the tables is Active. This is a bit value that is either true (1) or false (0) to indicate if the record has been deleted or not. One of the rules I was taught early in my development career is to retain all records and to set flags to indicate the state each record is in. Active allows records to be retained in the database, yet be considered deleted by the application. This is also pretty handy when a record was accidentally deleted by a user and needs to be restored. Simply flipping the bit back to true saves you from having to restore the record or, even worse, the entire database.

The tbh_Articles Table

The tbh_Articles table contains the content and all further information for all the articles in all categories. It is structured as follows.

Column Name

Type

Size

Allow Null

Description

ArticleID

int – PK

4

No

Unique ID for the article.

AddedDate

Datetime

8

No

Date/time the article was added.

AddedBy

Nvarchar

256

No

Name of the user who created the article.

CategoryID

int – FK

4

No

ID of the category to which the news item belongs.

Title

Nvarchar

256

No

Article's title.

Abstract

Nvarchar

4000

Yes

Article's abstract (short summary) to be shown in the page that lists the article, and in the RSS feed.

Body

Ntext

 

No

Article's content (full version).

Country

Nvarchar

256

Yes

Country to which the article (concert/event) refers.

State

nvarchar

256

Yes

State/province to which the article refers.

City

nvarchar

256

Yes

City to which the article refers.

ReleaseDate

datetime

8

Yes

Date/time the article will be publicly readable.

ExpireDate

datetime

8

Yes

Date/time the article will be retired and no longer readable by the public.

Approved

bit

1

No

Approved status of the article. If false, an administrator/editor has to approve the article before it is actually published and available to readers.

Listed

bit

1

No

Whether the article is listed in the articles page (indexed). If false, the article will not be listed, but will be still accessible if the user types the right URL, or if there is a direct link to it.

CommentsEnabled

bit

1

No

Whether the user can leave public comments on the article.

OnlyForMembers

bit

1

No

Whether the article is available to registered and authenticated users only or to everyone.

ViewCount

int

4

No

Number of times the article has been viewed.

Votes

int

4

No

Number of votes the article has received.

TotalRating

int

4

No

Total rating score the article has received. This is the sum of all the ratings posted by users.

Active

Bit

1

No

Indicates if the record has been deleted.

UpdateDate

Datetime

8

No

Datetime when the last update to the record was made.

UpdatedBy

nvarchar

256

No

Who made the last update to the record.

The ReleaseDate and ExpireDate columns are useful because the site's staff can prepare content in advance and postpone its publication, and then let the site update itself at the specified date/time. In addition to the obvious benefit of spreading out the workload, this is also great during vacation periods, when the staff would not be in the office to write new articles but you still want the site to publish fresh content regularly.

The Listed column is also very important, because it enables you to add articles that will be hidden from the main article list page, and from the RSS feeds. Why would you want to do this? Suppose that you have a category called Photo Galleries (we'll actually create it later in the chapter) in which you publish the photos of a past event or meeting. In such photo gallery articles, you would insert thumbnails of the photos with links to their full-sized version. It would be nice if the reader could comment and rate each and every photo, not just the article listing them all, right? You can do that if instead of linking the big photo directly you link a secondary article that includes the photo. However, if you have many photos, and thus many short articles that contain each of them, you certainly don't want to fill the category's article listing with a myriad of links to the single photos. Instead, you will want to list only the parent gallery. To do this, you set the Listed property of all the photo articles to false, and leave it set to true only on the article with the thumbnails.

The Country, State, and City fields enable you to specify an accurate location for those articles that refer to an event (such as parties, concerts, beer contests, etc.). You may recall that we created the same properties in Chapter 2 for the user's profile. If the location for the article matches a specific user's location, even partially, then you could highlight the article with a particular color when it's listed on the web page. You may be wondering why it was necessary to define the Country and State fields as varchar fields, instead of an int foreign key pointing to corresponding records of the tbh_Countries and tbh_States lookup tables. The answer is that I want to use the City field to support not only U.S. states, but states and provinces for any other country, so I defined this as free text field. It's also good for performance if we denormalize these fields. Using a lookup table is particularly useful when there is the possibility that some values may change; storing the information in one location minimizes the effort to update the data and makes it easier to ensure that we don't get out of sync. However, realistically, the list of countries will not change, so this isn't much of a problem. In the remote case that this might happen, you will simply execute a manual update for all those records that have Country="USA" instead of "United States", for example. This design decision can greatly improve the performance of the application.

Note

Be aware that IP address databases are available that can be used to determine the user's location as close as his ZIP Code in the United States. The targeting detail varies among these databases, but even one that gets the general area correct can be very useful for most sites to target the user's content.

You may be wondering why I decided to put the Votes and TotalRating columns into this table, instead of using a separate table to store all the single votes for all articles. That alternative has its advantages, surely: you could track the name and IP address of the user who submits the vote, and produce interesting statistics such as the number of votes for every level of rating (from one to five stars). However, retrieving the total number of votes, the total rating, and the number of votes for each rating level would require several SUM operations, in addition to the SELECT to the tbh_Articles table. I don't think the additional features are worth the additional processing time and traffic over the network, and thus I opted for this much lighter solution instead.

The tbh_Comments Table

The tbh_Comments table contains the feedback (comments, questions, answers, etc.) for the published articles. The structure is very simple:

Column Name

Type

Size

Allow Null

Description

CommentID

int - PK

4

No

Unique ID for the comment.

AddedDate

datetime

8

No

Date/time the comment was added.

AddedBy

nvarchar

256

No

Name of the user who wrote the comment.

AddedByEmail

nvarchar

256

No

User's e-mail address.

AddedByIP

nchar

15

No

User's IP address.

ArticleID

int

4

No

Article to which the comment refers.

Body

ntext

 

No

Text of the comment.

Active

Bit

1

No

Indicates if the record has been deleted.

UpdatedDate

Datetime

8

No

When the record was last updated.

UpdatedBy

nvarchar

256

No

The username of the user who last updated the record.

We will track the name of the user posting the comment, but she could even be an anonymous user, so this value will not necessarily be one of the registered usernames. We also store the user's e-mail address, so that the reader can be contacted with a private answer to her questions. Storing the IP address might be legally necessary in some cases, especially when you allow anonymous users to post content on a public site. In case of offensive or illegal content, it may be possible to geographically locate the user if you know her IP address and the time when the content was posted. In simpler cases, you may just block posts from that IP (not a useful option if it were a dynamically assigned IP, though).

Creating the Entity Model

Chapter 3 walked through creating an Entity Data Model using the Entity Framework Wizard. The same process holds for the articles model, but uses the tbh_Articles, tbh_Categories and tbh_Comments tables.

But, just as you saw with the SiteMap model, the Articles model needs to be customized. The thb_Articles entity needs to have the Entity Set Name set to Articles and the Name set to Article. The navigational properties need to be changed to Category and Comments, respectively. For the thb_Categories entity, change the Entity Set Name to Categories and the Name to Category. Rename the tbh_Articles navigational property to Article. Similary rename the tbh_Comments entity appropriately, so the final result looks like Figure 5-2.

Figure 5-2

Figure 5.2. Figure 5-2

Once the model is generated, the classes in the model need to be placed in the TheBeerHouse.BLL.Articles namespace. This requires editing the generated code file by adding the namespace wrapper around classes. This is done below the Assembly definitions at the top of the file, which means an Imports TheBeerHouse.BLL.Articles directive at the top of the page. That's because the Assembly definitions reference the classes, now wrapped in the Articles namespace. Each of these entities is extended in corresponding partial classes defined in the class library.

One property exposed by the Comment entity is EncodedBody, which returns the same text returned by the Body property, but first performs HTML encoding on it. This protects us against the so-called script-injection and cross-site scripting attacks. As a very simple example, consider a page on which you allow users to anonymously post a comment. If you don't validate the input, they may write something like the following:

<script>document.location = 'http://www.usersite.com';</script>

This text is sent to the server, and you save it into the DB. Later, when you have to show the comments, you would retrieve the original comment text and send to the browser as is. However, when you output the preceding text, it won't be considered as text by the browser, but rather as a JavaScript routine that redirects the user to another website, hijacking the user away from your website! And this was just a basic attack — more complex scripts could be used to steal users' cookies, which could include authentication tickets and personal data, with potentially grave consequences. For our protection, ASP.NET automatically validates the user input sent to the server during a postback, and checks whether it matches a pattern of suspicious text. If so, it raises an exception and shows an error page. You should consider the case where a legitimate user tries to insert some simple HTML just to format the text, or maybe hasn't really typed HTML but only a < character. In that case, you don't want to show an error page; you only need to ensure that the HTML code isn't displayed in a browser (because you don't want users to put links or images on your site, or text with a font so big that it creates a mess with your layout). To make sure it isn't displayed, you can disable ASP.NET's input validation (only for those pages on which the user is actually expected to insert text, not for all pages!), and save the text into the DB, but only show it on the page after HTML encoding, as follows:

&lt;script&gt; document.location = 'http://www.usersite.com'; &lt;/script&gt;

This way, text inserted by the user is actually shown on the page, instead of being considered HTML. The link will show as a link, but it will not be a clickable link, and no JavaScript can be run this way. The EncodedBody property returns the HTML encoded text, but it can't completely replace the Body property, because the original comment text is still required in certain situations — for example, in the administration pages where you show the text in a textbox and allow the administrator to edit it.

Scripting-based attacks must not be taken lightly, and you should ensure that your site is not vulnerable. One good reference on the web is www.technicalinfo.net/gunter/index.html, but you can easily find many others. Try searching for "XSS," using your favorite search engine.

There are two new features for the comment functionality of the Beer House, validating comments through Akismet and Gravatar support. Akismet is a comment spam-filtering service. Simply put a site can pass a comment through the Akismet API and it will return if Akismet thinks the comment is spam or not.

The last helper members help with accessing the user's Gravatar. Simply put, a Gravatar is a Globally Recognized Avatar and is managed at www.Gravatar.com. Anyone can go to Gravatar.com and create an account and assign images or photos to be used anywhere to represent them. If a commentor does not have a Gravatar account, the API returns a random image to represent the user, but more about that later, too.

Building Repository Classes

Instead of using stored procedures and a DAL (Data Access Layer) to access those stored procedures, with the Entity Framework and LINQ these tasks are contained within a repository class. As explained in Chapter 3, repository classes now manage the business aspect of the Beer House application. Repositories provide for some separation of concerns that will allow the architecture to be further extended in the future if desired. Figure 5-3 shows the class diagram for the Articles module.

Each of the entities related to the articles module has its own repository class. Each of these classes inherits a BaseRepository class that contains some common logic used by all the repository classes, such as the Dispose method and a common reference to the ArticlesEntities DataContext class to interact with the Entity Data Model for the articles module.

Figure 5-3

Figure 5.3. Figure 5-3

ArticleRepository

The ArticleRepository manages all the interactions with the Article entity, using LINQ to entities to build queries in the database. The repository is a class composed of methods to create, retrieve, update, and delete (CRUD) records in the tbh_Articles table, most manage retrieving and caching records in memory.

In the previous edition of the Beer House, the articles were retrieved in chunks or pages. This can still be done, but since the records are going to be cached in memory and pages using AJAX, this is not as important. If the site were to contain hundreds of thousands of records, then caching by page is slightly more feasible and can be done with LINQ by using the Skip and Take methods on the IQueryable(of Article) list returned by the LINQ query.

lArticles = (From lArticle In Articlesctx.Articles _
    Where lArticle.Published = True _
    Order By lArticle.ReleaseDate Descending).Skip(15).Take(15).ToList()

Skip accepts a numerical value that indicates how many rows to skip over before beginning the list of records to return. Similarly, the Take method accepts a numerical value to indicate how many records should be returned. So, the sample query returns the second displayed page of records.

Method

Description

GetArticles

Returns a list of Article instances and has three overloads to wrap all LINQ to Entities queries that retrieve the list of articles described above (to retrieve all articles, only published articles, etc.).

GetArticleCount

There are four overloads of this method that return the number of articles given no constraints (all articles), the parent category, the published status (but not the category), or the parent category plus the published status.

GetArticleByID

Returns an Article instance that fully describes the article identified by the input ID.

InsertArticle

Takes all the data for creating a new article, and returns its ID.

UpdateArticle

Updates data for an existing article and returns a Boolean value indicating whether or not the operation was successful.

DeleteArticle

Deletes the article identified by an ID and returns a Boolean value indicating whether or not the operation was successful.

ApproveArticle

Approves the article identified by an ID.

IncrementArticleViewCount

Increments the view count of the article identified by an ID.

RateArticle

Rates the article identified by the ID, with a value from 1 to 5.

GetPublishedArticles

A wrapper around the GetArticles overload passing a true value for the PublishedOnly parameter.

GetHomePageArticles

Returns a list of the three most recently added articles to have their titles and abstracts displayed on the home page.

The GetArticleById method returns a single article by its ID, but the method contains a couple of overloads. The primary returns just the article entity itself and does not retrieve the associated category or list of comments. This is the most efficient query but may not always meet the needs of the page asking for the article. In that case, the second overload accepts Boolean parameters to indicate if the associated category and comments should also be retrieved.

Public Function GetArticleById(ByVal ArticleId As Integer, ByVal
bIncludeCategories As Boolean, ByVal bIncludeComments As Boolean) As Article

            If ArticleId > 0 Then

                If bIncludeCategories And bIncludeComments Then

                    Return (From lai In
Articlesctx.Articles.Include("Categories").Include("Comments") _
                        Where lai.ArticleID = ArticleId).FirstOrDefault

                ElseIf bIncludeCategories And Not bIncludeComments Then

                  Return (From lai In Articlesctx.Articles.Include("Categories")
_ Where lai.ArticleID = ArticleId).FirstOrDefault

                ElseIf Not bIncludeCategories And Not bIncludeComments Then

                    Return (From lai In Articlesctx.Articles _
                        Where lai.ArticleID = ArticleId).FirstOrDefault

                End If

            End If

            Throw New ArgumentException("The ArticleId is not valid.")

End Function

The RateArticle method is not necessarily the most efficient use of SQL because it retrieves an instance of the article's entity and changes the TotalRating and Votes members, then commits the changes to the database. What is more efficient is to change the values in an existing article entity you may be working with and save that to the database, but that may not always be possible. So, this method performs that duty for you using LINQ.

Public Function RateArticle(ByVal ArticleId As Integer,
ByVal rating As Integer) As Boolean

            Dim lArticle As Article = GetArticleById(ArticleId)

            lArticle.TotalRating += rating
            lArticle.Votes += 1

            Try
                Articlesctx.SaveChanges()
                MyBase.PurgeCacheItems(CacheKey)
Return True
            Catch ex As OptimisticConcurrencyException
                ' catching this exception allows you to
                ' refresh entities with either store/client wins
                ' project the entities into this failed entities.
                Dim failedEntities = From e1 In ex.StateEntries _
                                     Select e1.Entity

                ' Note: in future you should be able to just pass the
opt.StateEntities in to refresh.
                Articlesctx.Refresh(RefreshMode.ClientWins,
failedEntities.ToList())
                Articlesctx.SaveChanges()

            Catch ex As Exception
                Return False
            End Try

End Function

The RateArticle method commits the changes to the database and also includes some special code to check for a OptimisticConcurrencyException and deals with it by specifying how the issue is to be resolved. While this should not be an issue with this method because the entity is being retrieved so fast, I thought it would be a good idea to include the code to demonstrate how this is done with the Entity Framework.

The IncrementArticleViewCount and ApproveArticle methods are structured in much the same way as the RateArticle method. They retrieve the article, change the relevant (ViewCount or Approved) value, and commit the change to the database.

The AddArticle method accepts an article entity and adds it to the database. There is a corresponding overload that accepts a series of parameters to create a new Article object. First, it checks to see if the Article already exists; if so, it uses the existing entity to make updates against. If it does not already exist, the method creates a new instance of an Article, and it is committed to the database by calling the other version of the AddArticle method.

Public Function AddArticle(ByVal articleID As Integer, ByVal title As String,
ByVal body As String, _
                ByVal approved As Boolean, ByVal listed As Boolean,
ByVal commentsEnabled As Boolean, _
                ByVal onlyForMembers As Boolean, ByVal viewCount As Integer,
ByVal votes As Integer, ByVal totalRating As Integer) As Article

            Dim article As Article

            If articleID > 0 Then

                article = GetArticleById(articleID)

                article.ArticleID = articleID
                article.UpdatedDate = Now
                article.UpdatedBy = Helpers.CurrentUserName
article.Title = title
                article.Body = body
                article.Approved = approved
                article.Listed = listed
                article.CommentsEnabled = commentsEnabled
                article.OnlyForMembers = onlyForMembers
                article.ViewCount = viewCount
                article.Votes = votes
                article.TotalRating = totalRating

            Else
                article = article.CreateArticle(articleID, Now,
Helpers.CurrentUserName, Now, True, title, body, _
                        approved, listed, commentsEnabled, onlyForMembers,
viewCount, votes, totalRating)
            End If

            Return AddArticle(article)

End Function

In the second AddArticle method leverages the built-in functionality of the Entity Framework to simply update the existing record. If the entity is not attached to the current context, it is added to the context before it is saved to the database. All the cache related to the Article entity is flushed from the system as well, to ensure that the freshest data is used by the application. Finally, a ternary If statement is used to return True or False, depending on if any records were affected, which typically will be true. If an exception occurs in the save operation, it is added to the ActiveExceptions list, which can then be used by the page working with the entity to inform the user about what happened.

Public Function AddArticle(ByVal vArticle As Article) As Article

            Try
                If vArticle.EntityState = EntityState.Detached Then
                    Articlesctx.AddToArticles(vArticle)
                End If
                MyBase.PurgeCacheItems(CacheKey)
                Return If(Articlesctx.SaveChanges > 0, vArticle, Nothing)

            Catch ex As Exception
                ActiveExceptions.Add(CacheKey & "_" & vArticle.ArticleID, ex)
                Return Nothing
            End Try

End Function

ActiveExceptions is a property defined in the BaseRepository class. It is a Dictionary of exceptions, so more than one exception can be caught and stored. I did this in case more than one exception was thrown, so that the page could loop through the exceptions and inform the user about each issue. Generally, there will be one exception, but there are some fringe cases where there might be more than one exception.

Private _activeExceptions As Dictionary(Of String, Exception)
Public Property ActiveExceptions() As Dictionary(Of String, Exception)
Get
                If IsNothing(_activeExceptions) Then
                    _activeExceptions = New Dictionary(Of String, Exception)
                End If
                Return _activeExceptions
            End Get
            Set(ByVal Value As Dictionary(Of String, Exception))
                _activeExceptions = Value
    End Set
End Property

The AddEdit pages in the site's admin all use the following routine to echo a list of the exceptions that occurred during a commit to the database:

Private Sub IndicateNotUpdated(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 = String.Format(My.Resources.EntityHasNotBeenUpdated,
"Article")
        End If

End Sub

DeleteArticle and UnDeleteArticle both call the ChangeDeletedState method but pass in either false to delete or true to undelete a record. The ChangeDeleteState method sets the Active field to the supplied vState parameter value, sets the UpdatedDate property to the current time and the UpdatedBy property to the current user's UserName.

Private Function ChangeDeletedState(ByVal vArticle As Article,
ByVal vState As Boolean) As Boolean
    vArticle.Active = vState
        vArticle.UpdatedDate = Now()
        vArticle.UpdatedBy = CurrentUserName

        Try
                Articlesctx.SaveChanges()
                MyBase.PurgeCacheItems(CacheKey)
                Return True
        Catch ex As Exception
                ActiveExceptions.Add(vArticle.ArticleID, ex)
                Return False
    End Try

End Function

CategoryRepository

The overall composition of the CategoryRepository class is very similar to the ArticleRepository as it applies to the basic CRUD operations.

Method

Description

GetCategories

Returns a list of Category Entities.

GetActiveCategories

Returns a list of all active Category Entities.

GetCategoryByID

Returns a Category entity by the CategoryID.

AddCategory

Takes all the data for creating a new category and returns true if the addition was successful.

UpdateCategory

Updates data for an existing category, and returns a Boolean value indicating whether the operation was successful.

DeleteCategory

Changes the Active flag for the category to indicate it is not active.

UnDeleteCategory

Changes the Active flag for the category to indicate it is active.

GetCategoryCount

Returns the number of categories stored in the database.

The CategoryRepository, located in the Articles folder of the class library, works much like the ArticleRepository. It does contain fewer members, since there are not many custom views of categories in the application. It differs from the previous edition of the Beer House; the AllArticles and PublishedArticles members have been removed and replaced with adequate corresponding members in the ArticleRepository class.

CommentRepository

The CommentRepository is a little simpler than the other two repository classes because it contains fewer methods and does not allow the deletion of comments. Instead, a comment is not going to be displayed unless it is approved.

Method

Description

GetComments

Returns a list of Comment Entities.

GetApprovedComments

Overloaded function that returns a list of all active Comment Entities, or all Comments by Article sorted in a desired order by date added.

GetCommentsByArticleId

Returns a list of Comments for an Article sorted in the specified order.

GetCommentCount

Returns the total number of Comments.

GetCommentByID

Returns a Comment entity by the CommentID.

AddComment

Takes all the data for creating a new comment and returns true if the addition was successful.

UpdateComment

Updates data for an existing comment and returns a Boolean value indicating whether the operation was successful.

ApproveComment

Changes the Approved flag for the Comment to indicate its approval state.

DeleteComment

Changes the Active flag for the Comment to indicate it has been deleted.

UnDeleteComment

Changes the Active flag for the Comment to indicate it has not been deleted.

The AddComment method does more than just add the comment to the database; it can also call the Akismet service to determine if the comment is spam or not. The method creates a new instance of the Akismet class, part of the Akismet.net library and passes an AkismetCommet object to the library, which subsequently calls Akisment to score the comment. If the comment is determined to be a spam comment, the method returns true; otherwise, it returns false to indicate the comment should be safe.

Working with Akismet requires having an account at WordPress.com, which is free to anyone who signs up (http://akismet.com). If you have higher traffic, there is a commercial license you might need to get, so check the Akismet website for more details.

The Akismet.net library has four methods that can be used to communicate with Akismet, VerifyKey, CommentCheck, SubmitSpam, and SubmitHam. If you want to verify your Word Press Key and blog, call the VerifyKey method, it returns True if you have a valid combination. To check a comment for spam, call the CommentCheck method, if Akismet returns false, then the comment should be safe. If you find items you want to submit to Akismet for their database, then use SubmitSpam, but only for something that is absolutely spam. Use the SubmitHam method if you are not quite sure if the comment is spam or not. Submitting suspect comments helps the Akismet system work better to protect the Internet from comment spam in the future!

We will not implement sorting features for the categories and the articles. This is because categories will always be sorted by importance (the Importance field) and then by name, whereas articles will always be sorted by release date, from the newest to the oldest, which is the right kind of sorting for these features. However, comments should be sorted in two different ways according to the situation:

  • From the oldest to the newest when they are listed on the user page, under the article itself, so that users will read them in chronological order, allowing them to follow a discussion made up of questions and answers between the readers and the article's author, or among different readers.

  • From the newest to the oldest in the administration page, so that the administrator finds the new comments at the top of the list, and in the first page (remember that comments support pagination), so they can be immediately read, edited, and, if necessary, deleted if found offensive.

Designing the Configuration Module

Chapter 3 introduced a custom configuration section named <theBeerHouse> that you must define in the root folder's web.config file, to specify some settings required in order for the site's modules to work. In that chapter, we also developed a configuration class that would handle the <contact> subelement of <theBeerHouse>, with settings for the Contact form in the Contact.aspx page. For the articles module of this chapter, you'll need some new settings that will be grouped into a new configuration subelement under <theBeerHouse>, called <articles>. This will be read by a class called ArticlesElement that will inherit from System.Configuration.ConfigurationElement and that will have the public properties shown in the following table.

Property

Description

ProviderType

Full name (namespace plus class name) of the concrete provider class that implements the data access code for a specific data store.

ConnectionStringName

Name of the entry in web.config's new <connectionStrings> section that contains the connection string to the module's database.

PageSize

Default number of articles listed per page. The user will be able to change the page size from the user interface.

RssItems

Number of items returned by the module's RSS feeds.

EnableCaching

Boolean value indicating whether the caching of data is enabled.

CacheDuration

Number of seconds for which the data is cached.

UrlIndicator

Used in the search engine friendly URLs to indicate the request is an article.

EnableTwitter

Boolean value indicating is posting to Twitter is enabled.

TwitterUserName

The site's Twitter account username.

TwitterPassword

The site's Twitter account password.

AkismetKey

Holds the Word Press Key needed to use Akismet.

EnableAkismet

Indicates if Akismet checking is turned on.

ReportAkismet

Indicates if automatic submission of comment spam to Akismet should be done.

The settings in the web.config file will have the same name, but will follow the camelCase naming convention; therefore, you will use providerType, connectionStringName, pageSize, and so on, as shown in the following example:

<theBeerHouse>
      <contactForm mailTo="[email protected]" />
      <articles pageSize="10" twitterUrserName="twitterBH"
twitterPassword="twitterPHPwd" enableTwitter="false" />
      :
</theBeerHouse>

A Helpers class is part of the BeerHouse class library that contains a series of shared (VB.NET) or static (C#) members. One of the members returns an instance of theBeerHouse configuration section.

Public Shared ReadOnly Settings As TheBeerHouseSection = _
    CType(WebConfigurationManager.GetSection("theBeerHouse"), TheBeerHouseSection)

An instance of ArticlesElement is returned by the Articles property of the TheBeerHouseSection class, described in Chapter 3, that represents the <theBeerHouse> parent section. This class will also have a couple of other new properties, DefaultConnectionStringName and DefaultCacheDuration, to provide default values for the module-specific ConnectionStringName and CacheDuration settings. These settings will be available for each module, but you want to be able to set them differently for each module. For example, you may want to use one database for storing articles data, and a second database to store forums data, so that you can easily back them up independently and with a different frequency according to how critical the data is and how often it changes. This is one of the reasons for having dedicated Entity Data Models for each module used in the site.

The same goes for the cache duration. However, in case you want to assign the same settings to all modules (which is probably what you will do for small to midsized sites), you can just assign the default values at the root section level, instead of copying and pasting them for every configuration subelement.

In addition to the properties listed earlier, the ArticlesElement will have another property, ConnectionString. This is a calculated property, though, not one that is read from web.config. It uses the <articles>'s connectionStringName or the <theBeerHouse>'s defaultConnectionStringName and then looks up the corresponding connection string in the web.config file's <connectionStrings> section, so the caller will get the final connection string and not just the name of its entry.

Designing the User Interface

The design of the ASP.NET pages in this module is not particularly special, so there's not much to discuss. We have a set of pages, some for the administrators and some for the end users, which allow us to manage articles, and navigate through categories and read articles, respectively. In the first edition of this book, the most important consideration for the UI section of the first chapters was the approach used to integrate the module-specific pages into the rest of the site. However, you've already seen from previous chapters that this is very straightforward in ASP.NET 2.0, thanks to master pages. Following are the pages we will code later:

  • ~/Admin/ManageCategories.aspx — Lists the current categories, and allows administrators to create new ones and delete and update existing ones.

  • ~/Admin/AddEditCategory.aspx — Allows administrators to create new categories and update existing categories.

  • ~/Admin/ManageArticles.aspx — Lists the current articles (with pagination support) and allows administrators to delete them. The creation of new articles and the editing of existing articles will be delegated to a secondary page.

  • ~/Admin/AddEditArticle.aspx — Allows administrators to create new articles and update existing articles.

  • ~/Admin/ManageComments.aspx — Lists the current comments for any article, has pagination support, and supports deletion and updates of existing comments.

  • ~/ShowCategories.aspx — An end-user page that lists all categories, with their title, description, and image.

  • ~/BrowseArticles.aspx — An end-user page that allows users to browse published articles for a specific category or for all categories. The page shows the title, abstract, author, release date, average rating, and location of the articles.

  • ~/ShowArticle.aspx — An end-user page that shows the complete article, along with the current comments at the bottom, and a box to let users post new comments and rate the article.

  • RSSFeed.vb — A custom HttpHandler that is mapped to handle all requests for any .RSS resource in the Beer House site. It is intelligent enough to apply appropriate filtering to the list of RSS items it serves.

Writing Articles with a WYSIWYG Text Editor

The first and most important challenge you face is that the site must be easily updatable by the client herself, without requiring help from any technical support people. Some regular employees working in the pub must be able to write and publish new articles, and make them look good by applying various formatting, colors, pictures, tables, and so forth. All this must be possible without knowing any HTML, of course! This problem can be solved by using a WYSIWYG (the acronym for "what you see is what you get") text editor: these editors enable users to write and format text, and to insert graphical elements, much like a typical word processor (which most people are familiar with), and the content is saved in HTML format that can be later shown on the end-user page "as is." There are various editors available, some commercial and some free. Among the different options I picked FCKeditor (www.fckeditor.net), mainly because it is open source and because it is compatible with most Internet browsers, including IE 5.5+, Firefox 1.0+, Mozilla 1.3+, and Netscape 7+. Figure 5-4 shows a screenshot of an online demo from the editor's website.

Figure 5-4

Figure 5.4. Figure 5-4

The editor is even localizable (language packs for many languages are already provided), and its user interface can be greatly customized, so you can easily decide what toolbars and what command buttons (and thus formatting and functions) you want to make available to users.

Uploading Files

The editor must be able to upload files, typically images for an article or to publish in a photo gallery, and maybe upload documents, screen savers, or other goodies that editors want to distribute to their end users. An administrator of a site would be able to use an FTP program to upload files, but an editor typically does not have the expertise, or the credentials, needed to access the remote server and its file system. An online file manager might be very helpful in this situation. In the first edition of this book, an entire chapter was devoted to showing you how to build a full-featured online file manager that would enable users to browse and remove folders and files; upload new files; download, rename, copy and delete existing files; and even edit the content of text files. However, this would be overkill in most situations, as the administrator is the only one who needs to have full control over the files and folders and structure of the site, and the administrator will presumably use an FTP client for this purpose. Editors and contributors only need the capability to upload new files. To implement this functionality, we will develop a small user control that allows users to upload one file at a time, and when done, displays the full URL of the file saved on the server, so the user can easily link to it using the WYSIWYG editor. The control will be used in various pages: in the page used to add and edit an article and in the page used to manage categories (as each category can have an image representing it); later in the book, we'll use this in the pages that send newsletters and submit forum posts.

This user control, named FileUploader.ascx, will utilize the new ASP.NET 2.0 FileUpload control to select the file, submit it, and save it on the server. This control simply translates to an <input type="file" /> control, with server-side methods to save the image. Under ASP.NET 1.x there was no such control; you had to add the runat="server" attribute to a plain HTML control declaration.

One important design decision we need to consider is how to avoid the possibility that different editors might upload files with the same name, overwriting previous files uploaded by someone else. A simple, but effective, solution is to save the file under ~/Uploads/{UserName}, where the {UserName} placeholder is replaced by the actual user's name. This works because only registered and authenticated users will have access to pages where they can upload files. We do want to let users overwrite a file that they uploaded themselves, as they might want to change the file.

Note

Remember that you will need to add NTFS write permission to the remote Uploads folder at deployment time, for the ASP.NET (Windows 2000 and XP) or Network Service user account (Windows Server 2003). It's easy to overlook this kind of thing, and you don't want to leave a bad impression with users when you set up a new site for them.

Article List User Control

You will need a way to quickly add the list of articles (with title, author, abstract, and a few more details) to any page. It's not enough to have entirely new articles; you also need to show them on existing pages so users will know about them! You'll need to show the list on the BrowseArticles.aspx page for end users and on the ManageArticles.aspx page for administrators. You may also want to show the article list on the home page. If you've got a good understanding of user controls, you may have already guessed that a user control is the best solution for this list because it enables us to encapsulate this functionality into a single code unit (the .ascx file plus the cs code-behind file), which enables us to write the code once and then place that user control on any page using one line of code.

This user control will be named ArticleListing.ascx. It produces different output according to whether the user is a regular user, an administrator, or an editor. If they belong to one of the special roles, each article item will have buttons to delete, edit, or approve them. This way, we can have a single control that will behave differently according to its context. Besides this, when the control is placed into an administration page, it must show all articles, including those that are not yet published (approved), or those that have already been retired (based on the date). When the control is on an end-user page, it must show only the active and published articles. The control will expose the following public properties (all Boolean), so that its content and its behavior can be changed in different pages:

Property

Description

EnableHighlighter

Indicates whether articles referring to events in the user's country, state/province, or city are highlighted with different colors.

PublishedOnly

Indicates whether the control lists only articles that are approved, and whose ReleaseDate–ExpireDate interval includes the current date.

RatingLockInterval

The number of days that must pass before a user can again rate the same article.

ShowCategoryPicker

Indicates whether the control shows a drop-down list filled with all article categories, which lets the user filter articles by category. If the property is false the drop-down list will be hidden, and the control will filter the articles by category according to the CategoryID parameter passed on the querystring.

ShowPageSizePicker

Indicates whether the control shows a drop-down list representing the number of articles to be listed per page. If the property is true, the user will be able to change the page size to a value that best meets his desires and his connection speed (users with a slow connection may prefer to have fewer items per page so that it loads faster).

EnablePaging

Indicates whether the control will paginate the collection of articles resulting from the current filters (category and published status). When false, the control will have no paging bar and will only show the first n articles, where n is the page size. This allows us to use the control on the home page, for example, to list the n most recent additions. When true, it will show only the first n articles but will also show an indication of which page is displayed, and the user can switch between pages of articles.

Producing and Consuming RSS Feeds

You've already learned from the introduction that we're going to implement a mechanism to provide the headlines of the site's new content as an RSS feed, so that external (online or desktop-based) aggregator programs can easily consume them, adding new content to their own site, but also driving new traffic to our site. This process of providing a list of articles via RSS is called syndication. The XML format used to contain RSS content is simple in nature (it's not an accident that the RSS acronym stands for "Really Simple Syndication"), and here's an example of one RSS feed that contains an entry for two different articles:

<rss version="2.0">
 <channel>
  <title>My RSS feed</title>
  <link>http://www.contoso.com</link>
  <description>A sample site with a sample RSS</description>
  <copyright>Copyright 2005 by myself</copyright>

  <item>
   <title>First article</title>
   <author>Marco</author>
   <description>Some abstract text here...</description>
   <link>http://www.contoso.com/article1.aspx</link>
   <pubDate>Sat, 03 Sep 2005 12:00:34 GMT</pubDate>
  </item>
  <item>
   <title>Second article</title>
   <author>Mary</author>
   <description>Some other abstract text here...</description>
   <link>http://www.contoso.com/article2.aspx</link>
   <pubDate>Mon, 05 Sep 2005 10:30:22 GMT</pubDate>
  </item>
 </channel>
</rss>

As you can see, the root node indicates the version of RSS used in this file, and just below that is a <channel> section, which represents the feed. It contains several required subelements, <title>, <link>, and <description>, whose names are self-descriptive. There can also be a number of optional subelements, including <copyright>, <webMaster>, <pubDate>, <image>, and others. After all those feed-level elements is the list of actual posts/articles/stories, represented by <item> subsections. An item can have a number of optional elements, a few of which (title, author, description, link, pubDate) are shown in the preceding example. For details on the full list of elements supported by RSS, you can check the link http://blogs.law.harvard.edu/tech/rss or just search for "RSS 2.0 Specification."

One important thing to remember is that this must be a valid XML format, and therefore you cannot insert HTML into the <description> element to provide a visual "look and feel," unless you ensure that it meets XML standards (XHTML is the name for tighter HTML that meets XML requirement). You must ensure that the HTML is well formed, so that all tags have their closing part (<p> has its </p>) or are self-closing (as in <img.../>), among other rules. If you don't want the hassle of making sure that the HTML is XML compliant, you can just wrap the text into a CDATA section, which can include any kind of data. Another small detail to observe is that the value for the pubDate elements must be in the exact format "ddd, dd MMM yyyy HH:mm:ss GMT," as in "Thu, 03 Jan 2002 10:20:30 GMT." If you aren't careful to meet these RSS requirements, your users may get errors when they try to view your RSS feed. Some feed readers are more tolerant than others, so it's not sufficient to make sure that it works in your own feed reader — you need to meet the RSS specifications.

A custom HttpHandler returns the RSS feed much more efficiently than a Web Form can. An HttpHandler is the foundational unit that any request processed by the ASP.NET engine uses to return any content. The Page class implements iHttpHandler, which requires a class implement two members, IsReusable and ProcessRequest.

Once you have an RSS feed for your site, you can also consume the feed on this site itself, on the home page, to provide a list of articles! The ArticleListing.ascx user control we already discussed is good for a page whose only purpose is to list articles, but it's too heavy for the home page. On the home page you don't need details such as the location, the rating, and other information. The new article's title and the abstract is enough — when users click on the title, they will be redirected to the page with the whole article, according to the link entry for that article in the RSS feed. We'll build our own RSS reader user control to consume our own RSS feed. The control will be generic, so that it will be able to consume RSS feeds from other sources as well, such as the site forum's RSS, the products RSS, or some other external RSS feeds. Its public properties are listed in the following table:

Property

Description

RssUrl

Full URL of the RSS feed.

Title

Title to be displayed.

MoreUrl

URL of a page with the full listing of items (versus the last n items returned by the RSS items). In the case of the articles module, this will be the URL for the BrowseArticles.aspx page.

MoreText

Text used for the link pointing to MoreUrl.

The only question left is how you can take the XML of the RSS feed and transform it into HTML to be shown on the page. Here are two possible solutions:

  • Use an XSL stylesheet to apply to the XML content, and use an XSLT transform to write the templates that define how to extract the content from the XML and represent it with HTML.

  • Dynamically build a DataTable with columns for the title, author, description, and the other <item>'s elements. Then fill the DataTable with the data read from the RSS feed, and use it as the data source for a Repeater, DataList, or other template-based control.

Personally, I strongly prefer the latter option, mostly because I find it much faster to change the template of a Repeater rather than to change the template in an XSL file. Additionally, with a DataTable I can easily add calculated columns, apply filters and sorting, and merge feeds coming from different sources (consider the case where you have multiple blogs or sources of articles and want to show their RSS feeds in a single box, with all items merged together and sorted by date). The DataTable approach is more flexible and easier to work with.

The Need for Security

The articles manager module is basically divided into two parts:

  • The administration section, which allows the webmaster, or another designated individual, to add, delete, or edit the categories, publish articles, and moderate comments.

  • The end-user section, which has pages to navigate through the categories, read the articles, rate an article or post feedback, and display the headlines on the home page.

Obviously, different pages may have different security constraints: an administration page should not be accessible by end users, and an article with the OnlyForMembers flag set should not be accessible by the anonymous users (users who aren't logged in). In the previous chapter, we developed a very flexible module that allows us to administer the registered users, read or edit their profile, and dynamically assign them to certain roles. For the articles manager module, we will need the following roles:

  • Administrators and Editors: These users have full control over the articles system. They can add, edit, or delete categories; approve and publish articles; and moderate comments. Only a very few people should belong to this role. (Note that Administrators also have full rights over the user management system and all the other modules of the site, so it might be wise if only a single individual has this role.)

  • Contributors: These users can submit their own articles, but they won't be published until an administrator or editor approves them. You could give this permission to many users if you wanted to gather as much content as possible, or just to a selected group of people.

Enforcing these security rules is a simple task, as you've learned in the previous chapter. In many cases, it suffices to protect an entire page against unauthorized users by writing some settings in that page's folder's web.config file. Settings done in a configuration file are called declarative coding, and settings made with source code are called imperative coding. I favor declarative coding because it's easier to modify without recompiling source code, but in some more complex cases you have to perform some security checks directly from code. An example is the AddEditArticle.aspx page, which is used to post a new article or edit an existing one. The first action (post) is available to Contributors and upper roles, while the second (edit) is available only to Editors and Administrators. When the page loads you must understand in which mode the page is being loaded, according to some querystring settings, and check the user's roles accordingly.

Solution

In coding the solution, we'll follow the same path we used in the "Design" section: from database tables and stored procedure creation, to the implementation of security, passing through the DAL, BLL, and lastly the user interface.

The Database Solution

Creating the database tables is straightforward with Visual Studio's integrated Server Explorer and database manager, so we won't cover it here. You can refer to the tables in the "Design" section to see all the settings for each field. In the downloadable code file for this book, you will find the complete DB ready to go. Instead, here you'll create relationships between the tables and write some stored procedures.

You create a new diagram from the Server Explorer: drill down from Data Connections to your database (if you don't see your database, you can add it as a new Data Connection), and then Database Diagrams. Right-click on Database Diagrams and select Add New Diagram. By following the wizard, you can add the tbh_Categories, tbh_Articles, and tbh_Comments tables to your diagram. As soon as the three tables are added to the underlying window, Server Explorer should recognize a relationship between tbh_Categories and tbh_Articles, and between tbh_Articles and tbh_Comments, and automatically create a parent-child relationship between them over the correct fields, as shown in Figure 5-5. However, if it does not, click on the tbh_Articles' CategoryID field and drag and drop the icons that appear over the tbh_Categories table. Once you release the button, a dialog with the relationship's properties appears, and you can ensure that the foreign key is the tbh_Articles' CategoryID field, while the primary key is tbh_Categories' CategoryID.

Figure 5-5

Figure 5.5. Figure 5-5

Now you have to create a relationship between tbh_Comments and tbh_Articles, based on the ArticleID field of both tables. As before, click the tbh_Comments' ArticleID field, drag and drop the icon over the tbh_Articles table, and complete the Properties dialog as before. When you're done with the diagram, go up to the tab, right-click on it, and save the diagram. Make sure that you let it change your tables as specified in the diagram.

Implementing the Configuration Module

The ArticlesElement class is implemented in the ~/App_Code/ConfigSection.cs file. It descends from System.Configuration.ConfigurationElement and implements the properties that map the attributes of the <articles> element under the <theBeerHouse> custom section in the web.config file. The properties, listed and described in the "Design" section, are bound to the XML settings by means of the ConfigurationProperty attribute. Here's its code:

Public Class ArticlesElement
    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("providerType",
DefaultValue:="MB.TheBeerHouse.DAL.SqlClient.SqlArticlesProvider")> _
    Public Property ProviderType() As String
        Get
            Return CStr(Me("providerType"))
        End Get
        Set(ByVal value As String)
            Me("providerType") = value
        End Set
    End Property

    <ConfigurationProperty("ratingLockInterval", DefaultValue:="15")> _
    Public Property RatingLockInterval() As Integer
        Get
            Return CInt(Me("ratingLockInterval"))
        End Get
        Set(ByVal value As Integer)
            Me("ratingLockInterval") = value
        End Set
    End Property

    <ConfigurationProperty("pageSize", DefaultValue:="10")> _
    Public Property PageSize() As Integer
        Get
Return CInt(Me("pageSize"))
        End Get
        Set(ByVal value As Integer)
            Me("pageSize") = value
        End Set
    End Property

    <ConfigurationProperty("rssItems", DefaultValue:="5")> _
    Public Property RssItems() As Integer
        Get
            Return CInt(Me("rssItems"))
        End Get
        Set(ByVal value As Integer)
            Me("rssItems") = 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
                Return duration
            Else
                Return Globals.Settings.DefaultCacheDuration
            End If
        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 = "Article"
            End If
            Return lurlIndicator
        End Get
        Set(ByVal Value As String)
            Me("urlIndicator") = Value
        End Set
End Property

    <ConfigurationProperty("akismetKey")> _
    Public Property AkismetKey() As String
        Get
            Dim lakismetKey As String = Me("akismetKey").ToString
            If String.IsNullOrEmpty(lakismetKey) Then
                lakismetKey = ""
            End If
            Return lakismetKey
        End Get
        Set(ByVal Value As String)
            Me("akismetKey") = Value
        End Set
    End Property

    <ConfigurationProperty("enableAkismet", DefaultValue:="false")> _
    Public Property EnableAkismet() As Boolean
        Get
            Return CBool(Me("enableAkismet"))
        End Get
        Set(ByVal value As Boolean)
            Me("enableAkismet") = value
        End Set
    End Property

    <ConfigurationProperty("reportAkismet", DefaultValue:="false")> _
    Public Property ReportAkismet() As Boolean
        Get
            Return CBool(Me("reportAkismet"))
        End Get
        Set(ByVal value As Boolean)
            Me("reportAkismet") = value
        End Set
    End Property

    <ConfigurationProperty("enableTwitter", DefaultValue:="false")> _
    Public Property EnableTwitter() As Boolean
        Get
            Return CBool(Me("enableTwitter"))
        End Get
        Set(ByVal value As Boolean)
            Me("enableTwitter") = value
        End Set
    End Property

    <ConfigurationProperty("twitterUrserName")> _
    Public Property TwitterUrserName() As String
        Get
            Dim lurlIndicator As String = Me("twitterUrserName").ToString
            If String.IsNullOrEmpty(lurlIndicator) Then
                lurlIndicator = "TwitterUserName"
            End If
            Return lurlIndicator
End Get
        Set(ByVal Value As String)
            Me("twitterUrserName") = Value
        End Set
    End Property

    <ConfigurationProperty("twitterPassword")> _
    Public Property TwitterPassword() As String
        Get
            Dim lurlIndicator As String = Me("twitterPassword").ToString
            If String.IsNullOrEmpty(lurlIndicator) Then
                lurlIndicator = "TwitterPassword"
            End If
            Return lurlIndicator
        End Get
        Set(ByVal Value As String)
            Me("twitterPassword") = Value
        End Set
    End Property

End Class

The ConnectionString property does not directly read/write a setting from/to the configuration file, but rather returns the value of the entry in the web.config's <connectionStrings> section identified by the name indicated in the <articles>'s connectionStringName attribute, or the <theBeerHouse>'s defaultConnectionStringName if the first setting is not present. The CacheDuration property returns the <articles>'s cacheDuration setting if it is greater than zero, or the <theBeerHouse>'s defaultCacheDuration setting otherwise.

DefaultConnectionStringName and DefaultCacheDuration are two new properties of the TheBeerHouseSection created in Chapter 3, now modified as shown here:

Public Class TheBeerHouseSection
    Inherits ConfigurationSection

    Private Shared ReadOnly instance As TheBeerHouseSection =
New TheBeerHouseSection

    <ConfigurationProperty("defaultConnectionStringName",
DefaultValue:="LocalSqlServer")> _
    Public Property DefaultConnectionStringName() As String
        Get
            Return CStr(Me("defaultConnectionStringName"))
        End Get
        Set(ByVal value As String)
            Me("DefaultConnectionStringName") = value
        End Set
    End Property

    <ConfigurationProperty("siteDomainName", DefaultValue:="localhost")> _
    Public Property SiteDomainName() As String
        Get
Return CStr(Me("siteDomainName"))
        End Get
        Set(ByVal value As String)
            Me("siteDomainName") = value
        End Set
    End Property

    <ConfigurationProperty("defaultCacheDuration", DefaultValue:="600")> _
    Public Property DefaultCacheDuration() As Integer
        Get
            Return CInt(Me("defaultCacheDuration"))
        End Get
        Set(ByVal value As Integer)
            Me("defaultCacheDuration") = value
        End Set
    End Property

    <ConfigurationProperty("contactForm", IsRequired:=True)> _
    Public ReadOnly Property ContactForm() As ContactFormElement
        Get
            Return CType(Me("contactForm"), ContactFormElement)
        End Get
    End Property

    <ConfigurationProperty("articles", IsRequired:=True)> _
    Public ReadOnly Property Articles() As ArticlesElement
        Get
            Return CType(Me("articles"), ArticlesElement)
        End Get
    End Property
End Class

The updated <theBeerHouse> section in web.config looks like this:

<theBeerHouse defaultConnectionStringName="TheBeerHouseEntities"
siteDomainName="http://TheBeerHouseBook.com">
<contactForm mailTo="[email protected]" />
<articles pageSize="10" twitterUrserName="TheBeerHouse" twitterPassword="132456"
enableTwitter="true" akismetKey="11aaaaa11111" enableAkismet="true"/>
</theBeerHouse>

To read the settings from code you can do it this way: Helpers.Settings.Articles.RssItems.

Implementing the Business Logic Layer

As we did for the data access classes, the business classes are created in the TBHBLL class library, which needs to be compiled before any changes can be used. The basic architecture of the business layer used in this edition is explained in Chapter 3. For the Articles module this includes a series of repository classes and extensions of the ObjectContext and Entity classes generated by the Entity Framework Wizard.

The Repositories

The Articles module has three main repository classes, one for Categories, Articles, and Comments. But because each of these repositories share the same entity data model and thus a common DataContext class and base CacheKey, the BaseArticleRepository manages these members. It also manages the Dispose method.

The BaseArticleRepository

The BaseArticleRepository inherits the BaseRepository that was discussed in Chapter 3. Its constructors are very similar to the SiteMapRepository; they set the connection string and set the core CacheKey value.

Public Sub New(ByVal sConnectionString As String)
ConnectionString = sConnectionString
CacheKey = "Articles"
End Sub

Public Sub New()
ConnectionString = Globals.Settings.DefaultConnectionStringName
CacheKey = "Articles"
End Sub

The balance of the BaseArticleRepository follows the same patterns discussed in Chapter 3, as will all the remaining repositories in the application.

The ArticleRepository

The ArticleRepository class, in the Articles folder of the class library (ArticlesRepository.vb) contains all the worker methods that interact with the articles entity model to manage articles. These include the common CRUD methods as well as some specialized methods to return records according to specific needs in the application. Each of these methods uses LINQ to entities as much as possible.

In the previous edition, the values related to paging had to be calculated by calling an article count stored procedure, the list of articles, and so forth. In this edition, I use the ListView and the DataPager control to manage paging the list of records for all the list views of data. The UpdatePanel also makes it easier to do "seamless" paging from the client by using AJAX queries to the server to perform databinding. All this combined with caching the records in memory means managing the image around paging is no longer a concern.

Public Function GetArticles() As List(Of Article) Implements
IArticleRepository.GetArticles
Dim key As String = String.Format(CacheKey)

If EnableCaching AndAlso Not IsNothing(Cache(key)) Then
Return CType(Cache(key), List(Of Article))
End If

Dim lArticles As List(Of Article) = (From lArticle In Articlesctx.Articles _
Order By lArticle.ReleaseDate Descending).ToList()

If EnableCaching Then
CacheData(key, lArticles)
End If

Return lArticles

End Function

The GetArticles method returns an unfiltered generic list of articles and caches the results in memory. This is a basic LINQ to Entities query that does order the articles by release date in descending order. the Entity Framework translates this LINQ query into the following SQL statement that is executed in the database:

SELECT
[Project1].[C1] AS [C1],
[Project1].[ArticleID] AS [ArticleID],
[Project1].[AddedDate] AS [AddedDate],
[Project1].[AddedBy] AS [AddedBy],
[Project1].[UpdatedDate] AS [UpdatedDate],
[Project1].[UpdatedBy] AS [UpdatedBy],
[Project1].[Active] AS [Active],
[Project1].[Title] AS [Title],
[Project1].[Abstract] AS [Abstract],
[Project1].[Body] AS [Body],
[Project1].[Country] AS [Country],
[Project1].[State] AS [State],
[Project1].[City] AS [City],
[Project1].[ReleaseDate] AS [ReleaseDate],
[Project1].[ExpireDate] AS [ExpireDate],
[Project1].[Approved] AS [Approved],
[Project1].[Listed] AS [Listed],
[Project1].[CommentsEnabled] AS [CommentsEnabled],
[Project1].[OnlyForMembers] AS [OnlyForMembers],
[Project1].[ViewCount] AS [ViewCount],
[Project1].[Votes] AS [Votes],
[Project1].[TotalRating] AS [TotalRating],
[Project1].[Keywords] AS [Keywords],
[Project1].[CategoryID] AS [CategoryID]
FROM ( SELECT
[Extent1].[ArticleID] AS [ArticleID],
[Extent1].[AddedDate] AS [AddedDate],
[Extent1].[AddedBy] AS [AddedBy],
[Extent1].[UpdatedDate] AS [UpdatedDate],
[Extent1].[UpdatedBy] AS [UpdatedBy],
[Extent1].[Active] AS [Active],
[Extent1].[CategoryID] AS [CategoryID],
[Extent1].[Title] AS [Title],
[Extent1].[Abstract] AS [Abstract],
[Extent1].[Body] AS [Body],
[Extent1].[Keywords] AS [Keywords],
[Extent1].[Country] AS [Country],
[Extent1].[State] AS [State],
[Extent1].[City] AS [City],
[Extent1].[ReleaseDate] AS [ReleaseDate],
[Extent1].[ExpireDate] AS [ExpireDate],
[Extent1].[Approved] AS [Approved],
[Extent1].[Listed] AS [Listed],
[Extent1].[CommentsEnabled] AS [CommentsEnabled],
[Extent1].[OnlyForMembers] AS [OnlyForMembers],
[Extent1].[ViewCount] AS [ViewCount],
[Extent1].[Votes] AS [Votes],
[Extent1].[TotalRating] AS [TotalRating],
1 AS [C1]
FROM [dbo].[tbh_Articles] AS [Extent1]
)  AS [Project1]
ORDER BY [Project1].[ReleaseDate] DESC

I am not an expert on optimized Transact SQL, but I do find it interesting to see almost the same SQL query nested inside itself to perform the sorting, since the Entity Framework has been designed to optimize SQL queries. Also notice that, instead of a Select * from statement, each field is explicitly stated. This is a requirement when you write Entity SQL; all fields in the query must be explicitly stated. You can monitor any activity in a SQL Server by running SQL Profiler, one of the tools you can install with SQL Server. Using this tool can help you understand how queries are being composed by Entity Framework to see where you might need to adjust a query.

I actually included three overloads of the GetArticles method so that you could see how to make various LINQ to Entities queries. The second overload accepts a parameter to filter for published articles, while the third adds pageIndex and pageSize parameters that allow you to retrieve just the desired pages of articles.

Another slight variation on the GetArticles methods is the GetHomePage articles method. It returns the three most recently published articles to be displayed on the home page. It filters for articles that are approved, listed, and have a release date that is before the current time. It then sorts by a descending date and takes the first three articles.

Public Function GetHomePageArticles() As List(Of Article)

            Dim lArticles As List(Of Article)

            Dim key As String = CacheKey & "_HomePage"

            If EnableCaching AndAlso Not IsNothing(Cache(key)) Then
                Return CType(Cache(key), List(Of Article))
            End If

            lArticles = (From lArticle In Articlesctx.Articles
.Include("Category") _
                Where lArticle.Active = True And lArticle.Approved = True And _
            lArticle.Listed = True And lArticle.ReleaseDate < Now() And
lArticle.ExpireDate > Now() _
            Order By lArticle.AddedDate Descending).Take(3).ToList()

            If EnableCaching Then
                CacheData(key, lArticles, CacheDuration)
            End If

            Return lArticles

End Function

The GetArticleCount method is a simple statement that uses the LINQ Count method to return the number of articles in the database. The problem with this approach is we might need to filter for the number of articles meeting specified criteria. In those cases, the Count member of the generic List class can be used to get a count of records matching the filter being applied to the database.

Public Function GetArticleCount() As Integer
Return Articlesctx.Articles.Count()
End Function

The CategoryRepository

The CategoryRepository class, in the Articles folder of the class library (CategoriesRepository.vb), contains all the worker methods that interact with the articles entity model to manage categories. These include the common CRUD methods as well as some specialized methods to return records according to specific needs in the application. Each of these methods uses LINQ to entities as much as possible. The members of this class use common LINQ query patterns that are explained in more detail under the ArticleRepository description.

The CommentRepository

The one special aspect of the CommentRepository class is the Add and Delete members. Each of these members interact with Akismet, assuming that this feature is enabled in the site's web.config.

The comment is still going to be added to the database, but instead of setting the initial Active flag to true, it is set to false. This means a site administrator can evaluate the comment to determine if it is really spam.

Public Function AddComment(ByVal vComment As Comment) As Comment

            Try

                If Helpers.Settings.Articles.EnableAkismet Then

                    Dim lAkismet As New Akismet(Helpers.Settings.Articles
.AkismetKey, _
                        "http://TheBeerHouseBook.com", "TheBeerHouse |
Akismet/1.11")

                    If lAkismet.CommentCheck(vComment.GetAkismetComment) Then
                        vComment.Active = False
                    End If

                End If

                If vComment.EntityState = EntityState.Detached Then
                    Articlesctx.AddToComments(vComment)
                End If
                Return If(Articlesctx.SaveChanges > 0, vComment, Nothing)

            Catch ex As Exception
                ActiveExceptions.Add(vComment.CommentID, ex)
                Return Nothing
End Try

End Function

The ApproveComment method accepts either a CommentId or a Comment entity and sets the Approved flag.

Public Function ApproveComment(ByVal lComment As Comment) As Boolean

            lComment.Approved = True
            lComment.UpdatedDate = Now()
            lComment.UpdatedBy = CurrentUserName

            Try
                Articlesctx.SaveChanges()
                Return True
            Catch ocEx As OptimisticConcurrencyException
                ActiveExceptions.Add(lComment.CommentID, ocEx)
                Return False
            Catch Ex As Exception
                ActiveExceptions.Add(lComment.CommentID, Ex)
                Return False
            End Try

End Function

When a comment is deleted, it can be assumed that it is spam in most cases. The Akismet API offers the ability for you to submit spam comments to add to its system for further analysis. How this analysis is applied to the scoring algorithm I am not sure. In case you do not want to automatically report deleted comments as spam, the ArticlesElement has a ReportAkismet Boolean property to turn that feature off.

Public Function DeleteComment(ByVal vComment As Comment) As Boolean

            If Helpers.Settings.Articles.EnableAkismet And
Helpers.Settings.Articles.ReportAkismet Then

                'Submit uncaught Comment Spam to Akismet to add to their database.
                Dim lAkismet As New
 Joel.Net.Akismet(Helpers.Settings.Articles.AkismetKey, _
                        "http://beerhouse.extremewebworks.com", "TheBeerHouse |
Akismet/1.11")
                lAkismet.SubmitSpam(vComment.GetAkismetComment)

            End If

            Return ChangeDeletedState(vComment, False)

End Function

In the previous edition, a compromise was made to programmatically sort the generic list of comments with a custom sorting method. LINQ makes this operation even easier by allowing us to apply LINQ syntax to sort the list of comments. The CommentRepository list retrieval methods have overloads to allow specification of which order in which the comments are listed in the returned generic list.

Public Function GetApprovedComments(ByVal ArticleId As Integer,
ByVal bMostRecentFirst As Boolean) As List(Of Comment)

            Dim key As String = CacheKey & "_Comments_Approved_ArticleId_" &
ArticleId & "_MostRecent_" & bMostRecentFirst

            If EnableCaching AndAlso Not IsNothing(Cache(key)) Then
                Return CType(Cache(key), List(Of Comment))
            End If

            Dim lComments As List(Of Comment) = DirectCast(Cache(key),
List(Of Comment))

            If IsNothing(lComments) Then

                Articlesctx.Comments.MergeOption = MergeOption.NoTracking

                If bMostRecentFirst Then
                    lComments = (From lComment In Articlesctx.Comments _
                             Where lComment.Approved = True And
lComment.Article.ArticleID = ArticleId _
                             Order By lComment.AddedDate Ascending).ToList()
                Else
                    lComments = (From lC In Articlesctx.Comments _
                             Where lC.Approved = True And lC.Article.ArticleID =
ArticleId _
                             Order By lC.AddedDate Descending).ToList()
                End If

            End If

            CacheData(key, lComments)
            Return lComments

End Function

The Article Class

The Article class is extended in the class library's Articles/Article.vb file. It is placed in the TheBeerHouse.BLL.Articles namespace. The Article class adds custom members, including immutable properties composed from values supplied by the entity framework. Since each entity class in the Entity Data Model is a partial class, they can be extended by creating another class with the same name in another file. They cannot be inherited by another class, they but can implement any interface needed. In the case of The Beer House, all entities implement the IBaseEntity interface. This means that the entity will implement an IsValid property and four methods to validate activity permission (CanRead, CanEdit, CanDelete, and CanAdd).

Public ReadOnly Property IsValid() As Boolean Implements IBaseEntity.IsValid
Get
If String.IsNullOrEmpty(Me.Title) = False And _
String.IsNullOrEmpty(Me.Abstract) = False And _
               String.IsNullOrEmpty(Me.Body) = False And _
               Me.ReleaseDate < Me.ExpireDate Then
           Return True
         End If
         Return False
End Get
End Property

The IsValid property determines if the entity has required values and they are in an acceptable state. For the Article entity, this means that it must have Title, Abstract, and Body values. The ReleaseData must also be less than the ExpireDate, or the article will never be published.

For the article module site, Administrators and Editors are allowed to add or edit article related entities. Each one of the entities in the articles model is extended to have authorization properties that can be used to verify action authorization.

Public ReadOnly Property CanAdd() As Boolean Implements BLL.IBaseEntity.CanAdd
            Get
                If Helpers.CurrentUser.IsInRole("Administrator") Or
Helpers.CurrentUser.IsInRole("Editor") Then
                    Return True
                End If
                Return False
            End Get
End Property


Public ReadOnly Property CanDelete() As Boolean Implements IBaseEntity.CanDelete
            Get
                If Helpers.CurrentUser.IsInRole("Administrator") Or
Helpers.CurrentUser.IsInRole("Editor") Then
                    Return True
                End If
                Return False
            End Get
End Property

Public ReadOnly Property CanEdit() As Boolean Implements IBaseEntity.CanEdit
Get
                If Helpers.CurrentUser.IsInRole("Administrator") Or
Helpers.CurrentUser.IsInRole("Editor") Then
                    Return True
                End If
Return False
End Get
End Property


Public ReadOnly Property CanRead() As Boolean Implements IBaseEntity.CanRead
Get
Return True
End Get
End Property

The Helpers.CurrentUser property returns an IPrincipal that represents the current user. It can be used to determine if the user belongs to a role or not.

Public Shared ReadOnly Property CurrentUser() As IPrincipal
        Get
            Return HttpContext.Current.User
        End Get
End Property

Each property of an entity has two events associated with it for changing values: [FieldName]Changing and [FieldName]Changed. The ArticleID property cannot be less than 0, so in the ArticleIDChanging event, a check to ensure the value is greater than 0 is done. If the value is negative, an ArgumentException is thrown.

Private Sub OnArticleIDChanging(ByVal value As Integer)
If value < 0 Then
      Throw New ArgumentException("The ArticleId cannot be less than 0.")
 End If

If value <> ArticleID Then
      IsDirty = True
 End If

End Sub

This idea can be carried over to any field in any entity in an Entity Data Model to verify that the value being set passes the requirements to be properly set.

While this is how validation is to be done based on the pattern generated by the Entity Data Model Wizard, I find it less than ideal because it requires throwing an exception when values do not meet the validation requirements. I think the IsValid method should be used to ultimately perform validation because it can be used before the entity is committed to the data store. Throwing exceptions is never an optimal way to perform data validation as values are being set. The IsValid property gives you the opportunity to stop the commitment and report the issues.

One of the drawbacks of the Entity Framework is access to foreign key values. The Entity Data Model for a database does not expose foreign key values. For the Article entity, this would be CategoryId since each article belongs to a category that is defined in the tbh_Categories table. Accessing this value requires a bit of work by referencing the EntityKey value. The Article entity has a CategoryReference property which can be used to access the foreign key value. It is of type System.Data.Objects.DataClasses.EntityReference(Of Category), which contains an EntityKey property. This property, in turn, contains a collection of EntityKeyValues, one for each member of the foreign key, which is typically one value. Since this value may not exist, you must check to see if the CategoryReference has been created before accessing it. In the case of the CategoryId, if there is no reference, then the property returns a 0.

Public Property CategoryId() As Integer
       Get
                If Not IsNothing(Me.CategoryReference.EntityKey) Then
                    Return Me.CategoryReference.EntityKey.EntityKeyValues(0).Value
                End If
Return 0
        End Get
        Set(ByVal Value As Integer)
                If Not IsNothing(Me.CategoryReference.EntityKey) Then
                    Me.CategoryReference = Nothing
                End If
                Me.CategoryReference.EntityKey = New
EntityKey("ArticlesEntities.Categories", "CategoryID", Value)

End Set

End Property

The EntityKey value can be set, but if there is an existing value, it must be destroyed and a new one created. In the case of the CategoryId, a new EntityKey is created by passing the entity object, the field name, and the value for the key.

If you are creating a new entity, the primary key value of the entity itself cannot be set; let the database create it for you. If you do set the value, an exception is thrown by the framework.

Carried over from the previous edition of the Beer House, immutable properties make up the Article entity. These are properties that are composed or calculated from values stored in the database for the article. Immutable properties for an article include the ArticleReleaseDate, AverageRating, CategoryTitle, Location, and Published.

The ArticleReleaseDate actually wraps around the ReleaseDate value because it can be null. The property is a string composed by calling the ToShortDateString property of the DateTime class. The ReleaseDate property is created as a nullable value, since the field can be null in the database. This can cause issues when dealing with the value in the user interface. This is solved by calling the GetValueOrDefault method and setting its value to a local variable.

Public ReadOnly Property ArticleReleaseDate() As String
            Get

                If Not IsNothing(ReleaseDate.GetValueOrDefault) Then
                    Dim lRelaseDate As DateTime = ReleaseDate.GetValueOrDefault
                    Return lRelaseDate.ToShortDateString
                End If

                Return String.Empty

            End Get
        End Property

        Public ReadOnly Property AverageRating() As Double
            Get
                If Me.Votes >= 1 Then
                    Return CDbl(Me.TotalRating) / CDbl(Me.Votes)
                Else
                    Return 0.0
                End If
            End Get
End Property

        Public ReadOnly Property CategoryTitle() As String
            Get
                If Not IsNothing(Me.Category) Then
                    Return Me.Category.Title
                End If
                Return String.Empty
            End Get
        End Property

        Public ReadOnly Property Location() As String
            Get
                Dim _location As String = String.Empty

                If Not IsNothing(Me.City) Then
                    _location = Me.City.Split(";")(0)
                End If

                If String.IsNullOrEmpty(Me.State) = False Then
                    If _location.Length > 0 Then
                        _location += ", "
                    End If
                    _location += Me.State.Split(";")(0)
                End If

                If String.IsNullOrEmpty(Me.Country) = False Then
                    If _location.Length > 0 Then
                        _location += ", "
                    End If
                    _location += Me.Country
                End If

                Return _location
            End Get
        End Property

        Public ReadOnly Property Published() As Boolean
            Get
                Return (Me.Approved And Me.ReleaseDate <= DateTime.Now And
Me.ExpireDate > DateTime.Now)
            End Get
End Property

The Category Class

The Category class has instance properties that fully describe a category of articles, instance methods to delete and update an existing category, and static methods to create, update, or delete one category. I won't describe all of them here as they are similar to the corresponding methods of the Article class. They're actually a little simpler because you don't need multiple overloads to support pagination and other filters. There are two properties, Articles and PublishedArticles, that use a couple of overloads of the Article.GetArticles static methods to return a list of Article objects. Like the Article.Comments property, these two properties also use the lazy load pattern, so articles are retrieved only once, when the property is read for the first time.

The IsValid property checks to make sure that the Category contains a Title and a positive Importance value.

Public ReadOnly Property IsValid() As Boolean Implements IBaseEntity.IsValid
            Get
                If String.IsNullOrEmpty(Title) And Importance > −1 Then
                    Return False
                End If
                Return True
            End Get
End Property

The authorization members are the same as the Article class:

#Region " Authorization "

        Public ReadOnly Property CanAdd() As Boolean Implements
BLL.IBaseEntity.CanAdd
            Get
                If Helpers.CurrentUser.IsInRole("Administrator") Or
Helpers.CurrentUser.IsInRole("Editor") Then
                    Return True
                End If
                Return False
            End Get
        End Property


        Public ReadOnly Property CanDelete() As Boolean Implements
IBaseEntity.CanDelete
            Get
                If Helpers.CurrentUser.IsInRole("Administrator") Or
Helpers.CurrentUser.IsInRole("Editor") Then
                    Return True
                End If
                Return False
            End Get
        End Property

        Public ReadOnly Property CanEdit() As Boolean Implements
IBaseEntity.CanEdit
            Get
                If Helpers.CurrentUser.IsInRole("Administrator") Or
Helpers.CurrentUser.IsInRole("Editor") Then
                    Return True
                End If
                Return False
            End Get
        End Property


        Public ReadOnly Property CanRead() As Boolean
Implements IBaseEntity.CanRead
            Get
                Return True
            End Get
        End Property

#End Region

The Comment Class

The Comment class is extended in several ways: to validate the data, give structured access to related data and to manage interactions with Akismet and Gravatar. The first is a property to allow managed access to the ArticleId foreign key value:

Public Property ArticleId() As Integer
Get
                If Not IsNothing(Me.ArticleReference.EntityKey) Then
                    Return Me.ArticleReference.EntityKey.EntityKeyValues(0).Value
                End If
                Return 0
End Get
Set(ByVal Value As Integer)
                If Not IsNothing(Me.ArticleReference.EntityKey) Then
                    Me.ArticleReference = Nothing
                End If
                Me.ArticleReference.EntityKey = New
EntityKey("ArticlesEntities.Articles", "ArticleID", Value)
End Set
End Property

Next is a ReadOnly property called DisplayTitle; it wraps access to the Article's Title. This property makes it easy to return the title if the related article has also been retrieved with the comment itself. But this is not always going to be the case, and to handle this a ternary If statement is used and "Not Supplied" is returned if the related Article does not exist.

Public ReadOnly Property DisplayTitle() As String
Get
Return If(String.IsNullOrEmpty(Me.Title), _
                    If(Not IsNothing(Me.Article), Me.Article.Title,
"Not Supplied"), Me.Title)
End Get
End Property

The EncodedBody property returns an HTMLEncoded version of the comment body to guard against cross-site scripting attacks:

Public ReadOnly Property EncodedBody() As String
Get
Return HttpUtility.HtmlEncode(Me.Body)
End Get
End Property

The IsValid property checks to make sure that the comment has a Body; if there is nothing in the body of the comment, there is no comment after all. But as I discussed earlier, documenting who made the comment is important for a comment on a website, so their e-mail and IP addresses must be submitted.

Public ReadOnly Property IsValid() As Boolean Implements IBaseEntity.IsValid
Get
If String.IsNullOrEmpty(Me.Body) = False And _
                    String.IsNullOrEmpty(Me.AddedByEmail) = False And _
                    String.IsNullOrEmpty(Me.AddedByIP) = False Then
                    Return True
         End If
         Return False
End Get
End Property

All comments must be scanned for spam because too many unscrupulous webmasters look to place unrelated comments on blogs trying to gain search engine placement and the occasional visitor. Akismet is a free scoring service available to anyone with a WordPress.com account. I will talk about that in more detail later in the chapter. But Akismet has a URL-based API that can be used to check a comment. For the Beer House, I decided to use a popular .NET library, Akismet.Net, available on CodePlex.com (www.codeplex.com/AkismetApi).

The Akismet.Net library requires that an AkismetComment class be created, which it then passes to Akismet for checking. The Comment entity has been extended to include the GetAkismetComment method, which builds an AkismetComment class:

Public Function GetAkismetComment() As AkismetComment

Dim lComment As New AkismetComment
lComment.Blog = "http://TheBeerHouseBook.com"
lComment.CommentAuthor = Me.AddedBy
lComment.CommentAuthorEmail = Me.AddedByEmail
lComment.CommentContent = Me.Body
lComment.CommentType = "comment"
lComment.UserIp = Me.AddedByIP
lComment.CommentAuthorUrl = Me.CommenterURL

Return lComment

End Function

The last custom property, EMailHash, returns a hash of the commentor's e-mail address. This is used to retrieve the user's Gravatar.

Public ReadOnly Property EMailHash() As String
Get
Return GravatarHelper.GetGravatarHash(Me.AddedByEmail)
End Get
End Property

Implementing Gravatars

Gravatar.com is a service that makes it easy for a person to store a standard Avatar in a central location. But it is also a very easy service to integrate into the Beer House site as an automatic service for visitors when they leave comments on the site or other content that may utilize an Avatar, such as the forums. The service works by calling a URL with a Hexidecimal MD5 hash of the commentor's e-mail address. The URL is in the form of http://www.gravatar.com/avatar/[EMail Hash].jpg?s=XY. The querystring is optional but is useful because it allows you to specify the image width. Additionally, you can pass a "d:" parameter to specify a default avatar to be used when one is not available for the user. More information can be found on the Gravatar implementation page, http://en.gravatar.com/site/implement/url.

Implementing the User Interface

The database design and the data access classes for the Articles module are now complete, so it's time to code the user interface. We will use the business classes to retrieve and manage Article data from the DB. We'll start by developing the administration console, so that we can use it later to add and manage sample records when we code and test the UI for end users.

The ManageCategories.aspx Page

The ManageCategories.aspx page, located under the ~/Admin folder, allows the administrator and editors to add, delete, and edit article categories, as well as directly jump to the list of articles for a specific category. The screenshot of the page, shown in Figure 5-6, demonstrates what I'm talking about, and then you'll learn how to build it.

Figure 5-6

Figure 5.6. Figure 5-6

Each of the list administrative pages in the Beer House will typically consist of a ListView with a DataPager wrapped in an UpdatePanel. The ListView is a much leaner tabular data control that works in tandem with the DataPage control to allow easy navigation through a set of entities. Wrapping the ListView inside an UpdatePanel quickly adds AJAX capabilities without changing any of the code from normal ASP.NET Web Form authoring.

These same administrative pages will also follow a common layout with a title displayed at the top and some relevant navigational links to other activities in that module. Typically, these links will take you to other lists in the module, such as Articles or Comments, or give you the ability to add a new record to the site.

The ListView used to manage the article categories displays the category's Title, an Edit icon (a pencil), and a Delete icon (a trash can). Clicking the Edit link takes you to the AddEditCategory.aspx page, where the category can be changed.

The ListView is a templated data control that works in a slightly different way than the ListView, DataList, and Repeater. It is more of a hybrid of the DataList and Repeater, with the option of paging and sorting built into it. The overall layout of the ListView is defined in the LayoutTemplate. The layout of the individual records is defined the ItemTemplate and the AlternatingItemTemplate. There is also an EmptyDataTemplate to display a message to the user if there are no items available to display. There are other templates available to handle in-place editing, and so forth, but they will not be used in the Beer House.

<asp:UpdatePanel runat="server" ID="uppnlCategories">
               <ContentTemplate>
                  <asp:ListView ID="lvCategories" runat="server" DataKeyNames=
"CategoryId">
                     <LayoutTemplate>
                        <table cellspacing="0" cellpadding="0"
class="AdminList">
      <thead>
                           <tr class="AdminListHeader">
                              <td>
                                 Title
                              </td>
                              <td>
                                 Edit
                              </td>
                              <td>
                                 Delete
                              </td>
                           </tr>
      </thead>
      <tbody>
                           <tr id="itemPlaceholder" runat="server">
                           </tr>
      </tbody>
                     <tfoot>
                        <tr>
                           <td colspan="3">
                              <div class="pager">
                                 <asp:DataPager ID="pagerBottom"
 runat="server" PageSize="15" PagedControlID="lvCategories">
                                    <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>
                           </td>
                        </tr>
                     </tfoot>
                  </table>
                     </LayoutTemplate>
                     <EmptyDataTemplate>
                        <tr>
                           <td colspan="3">
                                 Sorry there are no Categories
available at this time.
                           </td>
                        </tr>
                     </EmptyDataTemplate>
                     <ItemTemplate>
                        <tr>
                           <td class="ListTitle">
                              <a href='<%#
String.Format("AddEditCategory.aspx?categoryid={0}",
Eval("CategoryId")) %>'>
                                 <%# Eval("Title") %></a>
                           </td>
                           <td align="center">
                              <a href="<%#
String.Format("AddEditCategory.aspx?categoryid={0}",
Eval("CategoryId")) %>">
                                 <img src="../images/edit.gif" alt=""
width="16" height="16" class="AdminImg" /></a>
                           </td>
                           <td align="center">
                              <img src="../images/delete.gif" alt=""
width="16" height="16" class="AdminImg" />
                           </td>
                        </tr>
                     </ItemTemplate>
                  </asp:ListView>
</ContentTemplate>
</asp:UpdatePanel>

Inside the LayoutTemplate, one of the elements needs to be declared to runat the server and either have the name itemPlaceHolder or match a custom id supplied in the itemPlaceHolderID property of the ListView. This element will automatically be replaced by the Item and AlternatingItem templates with the appropriate databinding. You should also set the DataKeyNames property to the name of the entity's primary key value. This will be used in the deleting operation.

The DataPager can be declared inside or outside the LayoutTemplate. I like to place it in the LayoutTemplate because it gives more control over positioning. You can place it outside the template as long as the DataPager sets the PagedControlId property to the ListView. This means the DataPager can be placed anywhere on the page. You can have more than one pager assigned to the ListView.

Clicking the Delete icon calls the DeleteCategory member and reloads the categories in the ListView. When an item is deleted using a ListView the ListView's ItemDeleting event is called, then the ItemDeleted. You are still responsible for actually deleting the item and rebinding the list to the ListView.

Protected Sub lvCategories_ItemDeleting(ByVal sender As Object, ByVal e As
System.Web.UI.WebControls.ListViewDeleteEventArgs)
Handles lvCategories.ItemDeleting
        Using lCategoryrpt As New CategoryRepository
            lCategoryrpt.DeleteCategory(lvCategories.DataKeys(e.ItemIndex).Value)
            BindCategories()
        End Using
End Sub

In the ItemDeleting event handler a new CategoryRepository is created and the DeleteCategory method is called, passing the CategoryId of the record to be deleted. Then the BindCategories method is called to rebind the list. The CategoryId is retrieved from the ListView by accessing the DataKeys value for the selected row. This value was set by defining the DataKeyNames property in the ListView's declaration, DataKeyNames="CategoryId".

The ManageCategories.aspx.vb Code-Behind File

The code needed to manage the list of categories is very simple. The form's class inherits from the AdminPage class, which contains some helper members used by all the pages in the admin section. Specifically, for the administrative pages of the Articles module, the base admin page contains wrappers for the primary key parameters that are used to identify specific items. I will cover how that works in the AddEditCategory page.

The first thing the page does is check to ensure that the user is authorized to access this page. While the site's entire Admin folder is protected by the web.config security settings, it is always a good idea to check.

If the user is authorized, the BindCategories method is called. This method uses a CategoryRepository to bind a list of categories to the ListView.

Imports TheBeerHouse.UI
Imports TheBeerHouse.BLL.Articles
Imports System.Security

Partial Public Class ManageCategories
    Inherits AdminPage

    Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs)
Handles Me.Load

        If Not IsPostBack Then
            If Me.User.Identity.IsAuthenticated AndAlso _
                (Me.User.IsInRole("Administrators") Or _
                Me.User.IsInRole("Editors") Or _
                Me.User.IsInRole("Contributors") Or _
                Me.User.IsInRole("Posters")) Then

                BindCategories()
            Else
                Throw New SecurityException( _
                    "You are not allowed to edit existing articles!")
            End If

        End If

    End Sub

The BindCategories method creates a CategoryRepository, and the GetActiveCategories method returns a list of Categories that is bound to a list. This list is then bound to the ListView. As nice as the DataPager control is, it does not have a property to hide it when there is less than one page of records to display. So, I created a method, SetupListViewPager, and placed it in the BasePage class to handle this. It accepts the count of records and a reference to the DataPager. The SetUpListViewPager method then checks to see if the pager should be visible, which it should if there are more records being bound than records being displayed on a page. The method follows; remember this is not part of the ManageCategories.aspx page but part of the BasePage class.

Public Sub SetupListViewPager(ByVal vCount As Integer, ByVal vPager As DataPager)

     If Not IsNothing(vPager) Then
          vPager.Visible = IIf(vCount <= vPager.PageSize, False, True)
     End If

End Sub

You can see how this is applied in the BindCategories method:

Private Sub BindCategories()

        Using Categoryrpt As New CategoryRepository

            Dim lCategories As List(Of Category) =
Categoryrpt.GetActiveCategories()
            lvCategories.DataSource = lCategories
            lvCategories.DataBind()

            SetupListViewPager(lCategories.Count,
lvCategories.FindControl("pagerBottom"))

        End Using

    End Sub

The only task remaining is handling when the list is paged. The ListView control fires the PagePropertiesChanged event when the user pages the list. Since the ListView has a DataPager hooked into it, the ListView handles working with the records to display the desired set of records. Since the records are being cached in memory, this should be a pretty efficient way to manage paging through the records. If the site grows to have hundreds of thousands of categories, this strategy should be rethought. But for now it is sufficient.

Protected Sub lvCategories_PagePropertiesChanged(ByVal sender As Object,
ByVal e As System.EventArgs) Handles lvCategories.PagePropertiesChanged
        BindCategories()
    End Sub

End Class

The AddEditCategory.aspx Page

The AddEditCategory page (see Figure 5-7) displays the data in an editable format for a Category specified in the CategoryId QueryString parameter. The data is displayed in a form with appropriate controls, such as TextBoxes, DropDownList, FCKeditor, and so forth. If a new Category is being created, the controls are set to their blank state and the administrator can create a new Category.

Figure 5-7

Figure 5.7. Figure 5-7

I won't go into the details of the page's markup because there is nothing too fancy in play here. I will go over the standard AddEdit page markup with the AddEditArticle page because there are more interesting user interface elements on that page.

The AddEditCategories.aspx.vb Code-Behind File

The one particular item I want to discuss on the Category administration page is managing the uploading of the category's image. This is done with a FileUpload control, an ASP.NET web control that manages a file upload from the client. I created a method to manage the uploading of a file for an admin page called GetItemImage. It accepts a reference to a FileUpload and a TextBox that echoes the URL to the image. The first thing that the method does is check the FileUpload's HasFile property. If it is true, then there is a file being uploaded and the method saves the file to the "images" folder. The function then returns the URL reference to the stored image. If there is no file being uploaded, it checks to see if there is a value in the txtImageURL and returns that value. If none exists, it returns a reference to a stock image.

Protected Function GetItemImage(ByVal vFileUpload As FileUpload,
ByVal txtImageURL As TextBox) As String
            If vFileUpload.HasFile Then

                vFileUpload.SaveAs(Path.Combine( _
                    Path.Combine(Request.PhysicalApplicationPath, "images"),
vFileUpload.FileName))

                Return Path.Combine("~/images", vFileUpload.FileName)
            ElseIf String.IsNullOrEmpty(txtImageURL.Text) = False Then
                Return txtImageURL.Text
            Else
                Return "~/Images/pencil45_32.png"
            End If
End Function

The ManageArticles.aspx Page

Similarly to the ManageCategories.aspx page the ManageArticles.aspx page gives site administrators the capability to view a list of articles and go edit them. It also lists the Article title with an Edit and a Delete button, displayed in Figure 5-8.

Figure 5-8

Figure 5.8. Figure 5-8

It uses the same basic coding patterns used in the ManageCategory.aspx page.

The AddEditArticle.aspx Page

The AddEditArticle.aspx page allows Administrators, Editors, and Contributors to add new articles or edit existing ones. It decides whether to use edit or insert mode according to whether an ArticleID parameter was passed on the querystring. If it was, then the page loads in edit mode for that article, but only if the user is an Administrator or Editor (the edit mode is only available to Administrators and Editors, while the insert mode is also available to Contributors). This security check must be done programmatically, instead of declaratively from the web.config file. Figure 5-9 shows the page in edit mode for an article.

Figure 5-9

Figure 5.9. Figure 5-9

As you might guess from studying this picture, the page is a table with a series of input fields, a few read-only fields (ArticleID, AddedDate, and AddedBy as usual, but also ViewCount, Votes, and Rating) and many other editable fields. The Body field uses the open-source FCKeditor described earlier. It is declared on the page as any other custom control, but it requires some configuration first. To set up FCK, you must download two packages from www.fckeditor.net:

  1. FCKeditor (which at the time of writing is in version 2.6.3) includes the set of HTML pages and JavaScript files that implement the control. The control can be used not only with ASP.NET but also with ASP, JSP, PHP, and normal HTML pages. This first package includes the "host-independent" code, and some ASP/ASP.NET/JSP/PHP pages that implement an integrated file browser and file uploader.

  2. FCKedit.Net is the .NET custom control that wraps the HTML and JavaScript code of the editor.

Unzip the first package into an FCKeditor folder, underneath the site's root folder. Then unzip the second package and put the compiled DLL underneath the site's bin folder. The .NET custom control class has a number of properties that let you customize the look and feel of the editor; some properties can only be set in the fckconfig.js file found under the FCKeditor folder. The global FCKConfig JavaScript editor enables you to configure many properties, such as whether the File Browser and Image Upload commands are enabled. Here's how to disable them in code (we already have our own file uploader, so we don't want to use it here, and we don't want to use the file browser for security reasons):

FCKConfig.LinkBrowser = false;
FCKConfig.ImageBrowser = false;
FCKConfig.LinkUpload = false;
FCKConfig.ImageUpload = false;

You can even create a customized editor toolbar by adding the commands that you want to implement. For example, this shows how you can define a toolbar named "TheBeerHouse" with commands to format the text, and insert smileys and images, but not insert input controls or Flash animations:

FCKConfig.ToolbarSets["TheBeerHouse"] = [
 ['Source','Preview','Templates'],
 ['Cut','Copy','Paste','PasteText','PasteWord','-','Print','SpellCheck'],
 ['Undo','Redo','-','Find','Replace','-','SelectAll','RemoveFormat'],
 ['Bold','Italic','Underline','StrikeThrough','-','Subscript','Superscript'],
 ['OrderedList','UnorderedList','-','Outdent','Indent'],
 ['JustifyLeft','JustifyCenter','JustifyRight','JustifyFull'],
 ['Link','Unlink','Anchor'],
 ['Image','Table','Rule','Smiley','SpecialChar','UniversalKey'],
 ['Style','FontFormat','FontName','FontSize'],
 ['TextColor','BGColor'],
 ['About']
];

Many other configurations can be set directly from the ASP.NET page that hosts the control, as you'll see shortly.

The AddEditArticle.aspx page uses typical ASP.NET Web controls such as TextBoxes, DropDowns, and the like, but also leverages a few controls and extenders from the ASP.NET AJAX Control Toolkit, www.codeplex.com/AjaxControlToolkit. Normally, a control reference is added to the top of the page for a set of controls not included with the core .NET Framework, but a sitewide reference can be made in the controls section of the web.config file. Scott Guthrie details this technique on this blog, http://weblogs.asp.net/scottgu/archive/2006/11/26/tip-trick-how-to-register-user-controls-and-custom-controls-in-web-config.aspx:

<add tagPrefix="asp" namespace="AjaxControlToolkit" assembly="AjaxControlToolkit,
Version=3.0.20820.16598, Culture=neutral, PublicKeyToken=28f01b0e84b6d53e" />

Now the control toolkit has been registered for sitewide use the controls and extenders can be added to any form on the site as if they were common web controls. For articles, the CalendarExtender, MaskEditExtender, and MaskEditValidator are used. The CalendarExtender is used for the date related fields. The MaskEditExtender is used to limit the input a user can make to a valid date format. The MaskEditValidator enforces the data format on the client. If the user happens to enter something that does not match the mask, a error message is displayed, as has been done with the traditional validator controls since ASP.NET was introduced.

The following excerpt shows the use of the MaskEditExtendor, MaskEditValidator, and CalendarExtender to manage a datetime input on the AddEditArticle page:

<tr>
               <td>
                  Release Date :
               </td>
               <td>
                  <asp:TextBox runat="server" ID="txtReleaseDate" Width="70"
CssClass="formField"></asp:TextBox>
                  <asp:Image runat="Server" ID="iReleaseDate"
ImageUrl="~/images/Calendar.png" /><br />
                  <asp:CalendarExtender ID="CalendarExtender1" runat="server"
TargetControlID="txtReleaseDate"
                     PopupButtonID="iReleaseDate">
                  </asp:CalendarExtender>
                  <asp:MaskedEditExtender ID="MaskedEditExtender1"
runat="server" TargetControlID="txtReleaseDate"
                     Mask="99/99/9999" MessageValidatorTip="true"
OnFocusCssClass="MaskedEditFocus"
                     OnInvalidCssClass="MaskedEditError" MaskType="Date"
DisplayMoney="Left" AcceptNegative="Left" />
                  <asp:MaskedEditValidator ID="MaskedEditValidator1"
runat="server" ControlExtender="MaskedEditExtender1"
                     ControlToValidate="txtReleaseDate" IsValidEmpty="True"
EmptyValueMessage="Date is required"
                     InvalidValueMessage="Date is invalid"
ValidationGroup="Demo1" Display="Dynamic"
                     TooltipMessage="Input a Date" />
               </td>
            </tr>

The declaration of the FCKeditor shows the use of the ToolbarSet property, which references the TheBeerHouse toolbar defined earlier in the JavaScript configuration file.

If the user decides to cancel the operation, the button does a response redirect to the ManageArticles.aspx page. This is done by using a button HTML element with the client-side OnClick event handler loading the ManageArticles.aspx page.

<button onclick ="window.location.assign('ManageArticles.aspx'),"
value="">Cancel</button>

The AddEditArticle.aspx.vb Code-behind File

The AddEditArticle page allows Administrators, Editors, Contributors, and Posters to work with articles. However, if an article has been loaded, only site Administrators and Editors are allowed to perform this action. If an ArticleId is passed through the QueryString the BindArticle method is called and the article's data is bound to the controls on the page. If the user does not belong to these roles, the page will throw a SecurityException:

Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs)
Handles Me.Load

        If Not IsPostBack Then

            If Me.User.Identity.IsAuthenticated AndAlso _
                (Me.User.IsInRole("Administrators") Or _
                Me.User.IsInRole("Editors") Or _
                Me.User.IsInRole("Contributors") Or _
                Me.User.IsInRole("Posters")) Then

                txtBody.BasePath = Me.BaseUrl & "FCKeditor/"

                If ArticleId > 0 Then
                    If Me.User.IsInRole("Administrators") Or _
                        Me.User.IsInRole("Editors") Then
                        BindArticle()
                    Else
                        Throw New SecurityException( _
                            "You are not allowed to edit existent articles!")
                    End If

                Else
                    ClearItems()
                End If

            Else
                Throw New SecurityException( _
                    "You are not allowed to edit existent or insert new articles!")
            End If

        End If

End Sub

If the page was loaded to create a new article the ClearItems method is called, which sets all the input controls to either empty (TextBox for example) or to a default selection (DropDownList for example). If the user is not a member of any of the preceding groups, a SecurityException is thrown, but most likely there are some other security issues with the site, since the Admin section of the site is closed off to anyone not in these groups in the web.config file!

The FCKeditor requires another property, BasePath, that points to the URL of the FCKeditor folder that contains all its HTML, JavaScript, and image files. This is also done in the page load event handler.

The ClearItems method is called when an article is being created. This clears all the TextBoxes and sets other controls to their initial state. You should preselect the checkboxes that make the article listed and to allow comments. You should also select and enable the Approved checkbox, but only if the current user belongs to the Administrators or Editors roles. This can be done by setting a local Boolean variable to the result of checking the user Role membership. The status Literal control should also have its text set to "Create a New Article" to describe what the user is doing:

Private Sub ClearItems()

        ltlArticleID.Text = String.Empty
        ltlAddedDate.Text = String.Empty
        ltlAddedBy.Text = String.Empty

        txtTitle.Text = String.Empty
        txtAbstract.Text = String.Empty
        txtBody.Value = String.Empty
        ddlState.ClearSelection()
        ddlCountry.ClearSelection()
        txtCity.Text = String.Empty
        txtReleaseDate.Text = String.Empty
        txtExpireDate.Text = String.Empty
        Dim bApprove As Boolean = (Me.User.IsInRole("Administrators") Or _
                        Me.User.IsInRole("Editors"))
        cbApproved.Checked = bApprove
        cbApproved.Enabled = bApprove
        cbListed.Checked = True
        cbCommentsEnabled.Checked = True
        cbOnlyForMembers.Checked = False
        ArticleHelper.BindCategoriesToListControl(ddlCategories, 0)

        ltlViewCount.Text = "0"
        ltlVotes.Text = "0"
        ltlRating.Text = "0"

        ltlUpdatedDate.Text = String.Empty
        ltlUpdateBy.Text = String.Empty

        ltlStatus.Text = "Create a New Article"

End Sub

Notice the use of the ArticleHelper.BindCategoriesToListControl method. This is a little helper method that is useful to the Articles module for binding a list of categories to a DropDownList control. This method accepts a ListControl, which is the base class for several controls such as the DropDownList, and a selectedId or the CategoryId to be the selected Category. It then binds a list of active categories to the control and sets the selected value.

Public Shared Sub BindCategoriesToListControl(ByVal lCtrl As ListControl,
ByVal selectedId As Integer)

        Using Categoryrpt As New CategoryRepository

            lCtrl.DataTextField = "Title"
            lCtrl.DataValueField = "CategoryID"

            lCtrl.DataSource = Categoryrpt.GetActiveCategories
lCtrl.DataBind()

            lCtrl.SelectedValue = selectedId

        End Using

End Sub

The BindArticle method binds the requested article, based on the ArticleId to the controls on the page:

Private Sub BindArticle()

        Using Articlerpt As New ArticleRepository

            Dim lArticle As Article = Articlerpt.GetArticleById(ArticleId, False,
False)

            If Not IsNothing(lArticle) Then

                ltlArticleID.Text = lArticle.ArticleID
                ltlAddedDate.Text = lArticle.AddedDate
                ltlAddedBy.Text = lArticle.AddedBy

                txtTitle.Text = lArticle.Title
                txtAbstract.Text = lArticle.Abstract
                txtKeywords.Text = lArticle.Keywords
                txtBody.Value = lArticle.Body
                ddlState.SelectedValue = lArticle.State
                ddlCountry.SelectedValue = lArticle.Country
                txtCity.Text = lArticle.City
                txtReleaseDate.Text = lArticle.ReleaseDate
                txtExpireDate.Text = lArticle.ExpireDate
                cbApproved.Checked = lArticle.Approved
                cbListed.Checked = lArticle.Listed
                cbCommentsEnabled.Checked = lArticle.CommentsEnabled
                cbOnlyForMembers.Checked = lArticle.OnlyForMembers

                ArticleHelper.BindCategoriesToListControl(ddlCategories,
lArticle.CategoryId)

                ltlViewCount.Text = lArticle.ViewCount
                ltlVotes.Text = lArticle.Votes
                ltlRating.Text = lArticle.AverageRating
                ltlUpdatedDate.Text = lArticle.UpdatedDate
                ltlUpdateBy.Text = lArticle.UpdatedBy

                ltlStatus.Text = "Edit The Article."
            Else
                ArticleId = 0
                ltlStatus.Text = "Create a New Article."
            End If

        End Using

End Sub

The FileUploader.ascx User Control

The FileUploader.ascx user control, located under the ~/Controls folder, allows administrators and editors to upload a file (normally an image file) to the server and save it in their own private user-specific folder. Once the file is saved, the control displays the URL so that the editor can easily copy and paste it into the ImageUrl field for a property, or reference the image file in the article's WYSIWYG editor. The markup code is simple — it just declares an instance of the FileUpload control, a Submit button, and a couple of Labels for the positive or negative feedback:

Upload a file:
<asp:FileUpload ID="filUpload" runat="server" />&nbsp;
<asp:Button ID="btnUpload" runat="server" OnClick="btnUpload_Click"
   Text="Upload" CausesValidation="false" /><br />
<asp:Label ID="lblFeedbackOK" SkinID="FeedbackOK" runat="server"></asp:Label>
<asp:Label ID="lblFeedbackKO" SkinID="FeedbackKO" runat="server"></asp:Label>

The file is saved in the code-behind's btnUpload_Click event handler, into a user-specific folder under the ~/Uploads folder. The actual saving is done by calling the SaveAs method of the FileUpload's PostedFile object property. If the folder doesn't already exist, it is created by means of the System.IO.Directory.CreateDirectory static method:

Protected Sub btnUpload_Click(ByVal sender As Object, ByVal e As System.EventArgs)
 Handles btnUpload.Click
        If Not filUpload.HasFile Then
            Try
                ' if not already present, create a directory
                ' names /uploads/{CurrentUserName}
                Dim dirUrl As String = CType(Me.Page, BasePage).BaseUrl + _
                    "Uploads/" + Me.Page.User.Identity.Name
                Dim dirPath As String = Server.MapPath(dirUrl)
                If Not Directory.Exists(dirPath) Then
                    Directory.CreateDirectory(dirPath)
                End If
                ' save the file under the users's personal folder
                Dim fileUrl As String = dirUrl + "/" + _
                    Path.GetFileName(filUpload.PostedFile.FileName)
                filUpload.PostedFile.SaveAs(Server.MapPath(fileUrl))

                lblFeedbackOK.Visible = True
                lblFeedbackOK.Text = "File successfully uploaded: " + fileUrl
            Catch ex As Exception
                lblFeedbackKO.Visible = True
                lblFeedbackKO.Text = ex.Message
            End Try
        End If
End Sub

To register this control on a page, you write the following directive at the top of the page (for example, the ManageCategories.aspx page):

<%@ Register Src="~/Controls/FileUploader.ascx"
   TagName=" FileUploader" TagPrefix="mb" %>

And use this tag to create an instance of the control:

<mb:FileUploader ID="FileUploader1" runat="server" />

The ManageComments.aspx Page

The ManageComments.aspx page is located under the ~/Admin/ folder, and it displays all comments of all articles, from the newest to the oldest, and allows an administrator or an editor to moderate the feedback by editing or deleting comments that may not be considered suitable. The page uses a pagable ListView for displaying the comments. Figure 5-10 shows this page.

Figure 5-10

Figure 5.10. Figure 5-10

I won't cover the code for this page in detail because it's similar to other code that's already been discussed. The pagination for the ListView is implemented the same way as the pagination in the ArticleListing user control. You can refer to the downloadable code for the complete implementation.

There are a couple of visual differences from the majority of other admin list interfaces to note. The edit column is titled Approve because that is all you can do. I chose not to make this an inline process with AJAX because the comment itself is not displayed and I want to force the editor to read the comment before making the decision to ban it. The delete column is also absent for this same reason. I chose not to display comments in the grid because sometimes they can be rather long for this type of interface. Finally, the last column displays a graphic to indicate if the comment is approved or not. The following code shows the ListView, wrapped in an UpdatePanel. To view the full code, open the source file in Visual Studio.

<asp:UpdatePanel runat="server" ID="uppnlComments">
                    <ContentTemplate>
                        <asp:ListView ID="lvComments" runat="server">
                            <LayoutTemplate>
...
                            </LayoutTemplate>
                            <EmptyDataTemplate>
...
                            </EmptyDataTemplate>
                            <ItemTemplate>
...
                            </ItemTemplate>
                            <AlternatingItemTemplate>
...
                            </AlternatingItemTemplate>
                        </asp:ListView>
                        <div class="pager">
...
                        </div>
                    </ContentTemplate>
                </asp:UpdatePanel>

The AddEditComment.aspx File

While the title of the AddEditComment.aspx page is slightly misleading, I wanted to keep a consistent naming convention for the details page. This page (see Figure 5-11) allows only administrators to approve or delete comments.

Figure 5-11

Figure 5.11. Figure 5-11

The only actionable control on the page is the Approved checkbox. This can either be true or false and is set from the value stored in the database. The Delete function flips the Active bit to false and also reports the comment to Akismet. This was covered in the section on the CommentRepository earlier, so check that out for more details.

The ShowCategories.aspx Page

ShowCategories.aspx is the first end-user page of this module, located in the site's root folder. Its only purpose is to display the article categories in a nice format, so that the reader can easily and clearly understand what the various categories are about and quickly jump to their content by clicking on the category's title. Figure 5-12 shows this page.

Figure 5-12

Figure 5.12. Figure 5-12

The list of categories is implemented as a ListView using grouping, with two columns (with GroupItemCount set to 2). We don't need pagination, sorting, or editing features here, so a simple ListView with support for repeated columns is adequate. Following is the code for the ListView:

<asp:UpdatePanel runat="server" ID="uppnlArticles">
     <ContentTemplate>
        <asp:ListView ID="lvCategories" runat="server" DataKeyField="CategoryID"
GroupItemCount="2">
           <LayoutTemplate>
              <table id="tblCategories" runat="server" cellspacing="0"
cellpadding="6" style="width: 100%;">
                 <tr runat="server" id="groupPlaceholder" />
              </table>
              <div class="pager">
                 <asp:DataPager ID="pagerBottom" runat="server" PageSize="4"
PagedControlID="lvCategories">
                    <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>
           </LayoutTemplate>
           <GroupTemplate>
              <tr runat="server" id="CategoriesRow" style="background-color:
#FFFFFF">
                 <td runat="server" id="itemPlaceholder" />
              </tr>
           </GroupTemplate>
           <ItemTemplate>
              <td style="width: 1px">
                 <asp:HyperLink ID="lnkCatImage" runat="server" NavigateUrl='<%#
"BrowseArticles.aspx?CategoryID=" & Eval("CategoryID") %>'>
                    <asp:Image ID="imgCategory" runat="server" BorderWidth="0px"
AlternateText='<%# Eval("Title") %>'
                       ImageUrl='<%# Eval("ImageUrl") %>' /></asp:HyperLink>
              </td>
              <td>
                 <div class="sectionsubtitle">
                     <a class="articletitle" href='<%# SEOFriendlyURL( _
                             Path.Combine("Category", Eval("Title")), ".aspx") %>'>
                       <%# httputility.HTMLEncode(Eval("Title")) %></a>
                    <br />
                    <asp:Literal ID="lblDescription" runat="server" Text='<%#
Eval("Description") %>'></asp:Literal>
                    <a class="articletitle" href='<%# SEOFriendlyURL( _
                             Path.Combine("Category", Eval("Title")), ".rss") %>'>
                       <img src="Images/rss.gif" alt="Get the Rss for this
category" style="border-width: 0px;" /></a>
              </td>
           </ItemTemplate>
        </asp:ListView>
     </ContentTemplate>
  </asp:UpdatePanel>

The category's image and title both link to the BrowseArticles.aspx page, with a CategoryID parameter on the querystring equal to the CategoryID of the clicked row. If you look at the code, you will notice that a direct URL is not formulated, but rather there is a search engine friendly URL for both the BrowseArticles page and the corresponding RSS feed. The code-behind for this page contains nothing new to review.

The BrowseArticles.aspx Page

BrowseArticles.aspx is the end-user version of the ManageArticles.aspx page presented earlier. It shows only published content instead of all content, but otherwise it's the same as ManageArticles.aspx; it just declares an instance of the shared ArticleListing user control:

<mb:ArticleListing id="ArticleListing1" runat="server" PublishedOnly="True" />

Figure 5-13 represents a screenshot of the page. Note that the page is available to all users, but because the current user is anonymous and the two articles listed on the page have their OnlyForMembers property set to false, the key image is shown next to them. If the user clicks the article's title, she will be redirected to the login page.

Figure 5-13

Figure 5.13. Figure 5-13

The ShowArticle.aspx Page

The ShowArticle.aspx end-user page outputs the article's entire text and all its other information (author, release date, average rating, number of views, etc.). It also lets the user rate the article (from one to five glasses of beer) and to submit comments. All comments are listed in chronological order, on a single page, so that it's easy to follow the discussion. When the page is loaded by an editor or an administrator, some additional buttons to delete, approve, and edit the article are also visible. Figure 5-14 shows a screenshot of the page as seen by an end user.

Figure 5-14

Figure 5.14. Figure 5-14

The Administrative links are displayed if the user is a site Administrator or an Editor. The Edit link is a HyperLink control that points to the AddEditArticle.aspx page in the site's Admin folder. The Approve and Delete links are LinkButton controls that post back to the server and perform the associated task. In the case of the Approve link, it is only displayed if the article has not been approved yet.

If Me.User.Identity.IsAuthenticated AndAlso _
(Me.User.IsInRole("Administrators") Or _
Me.User.IsInRole("Editors")) Then

hlnkEdit.NavigateUrl = String.Format("~/Admin/AddEditArticle.aspx?ArticcleId={0}",
 ArticleId)
hlnkEdit.Visible = True

lbtnApprove.Visible = IIf(lArticle.Approved, False, True)
lbtnDelete.Visible = True

End If

Approving and deleting an article are handled by using an ArticleRepository and calling the associated members. When an article is deleted, the user is redirected to the ShowCategories.aspx page.

Private Sub lbtnDelete_Click(ByVal sender As Object, ByVal e As System.EventArgs)
Handles lbtnDelete.Click
        Using lArticlerpt As New ArticleRepository

            lArticlerpt.DeleteArticle(ArticleId)
            Response.Redirect("~/ShowCategories.aspx")

        End Using
End Sub

Private Sub lbtnApprove_Click(ByVal sender As Object, ByVal e As System.EventArgs)
 Handles lbtnApprove.Click
        Using lArticlerpt As New ArticleRepository
            lArticlerpt.ApproveArticle(ArticleId)
            lbtnApprove.Visible = False
        End Using
End Sub

Another control available in the ASP.NET AJAX Control Toolkit is the Rating control. It provides a much improved way to collect and display ratings for articles. In the case of the Beer House, I decided to use beer mugs as a visual indicator of how readers liked the article. Again, since the Control Toolkit has been registered in the web.config file, the Rating control can be added just like any other control using the asp prefix. I did wrap it up in an update panel to make the rating a seamless process:

<asp:UpdatePanel ID="UpdatePanel1" runat="server">
<ContentTemplate>Rate This Article:<br />
<asp:Rating ID="ArticleRating" runat="server" BehaviorID="RatingBehavior1"
CssClass="ArticleRating" StarCssClass="ratingStar"
WaitingStarCssClass="savedRatingStar" FilledStarCssClass="filledRatingStar"
                            EmptyStarCssClass="emptyRatingStar">
</asp:Rating>
</ContentTemplate>
</asp:UpdatePanel>

The Rating control displays images based on the style assigned to corresponding properties of the control. An empty value is the StarCssClass, and a filled value is the FilledStarCssClass. These are defined in the stylesheet.

/* Rating */

.ArticleRating
{
    float: right;
    margin:3px 75px 10px 10px;
}

.ratingStar {
    font-size: 0pt;
    width: 19px;
    height: 19px;
margin: 0px;
    padding: 0px;
    cursor: pointer;
    display: block;
    background-repeat: no-repeat;
}

.filledRatingStar {
    background-image: url(Images/filledbeer.gif);

}

.emptyRatingStar {
    background-image: url(Images/EmptyBeer.gif);
}

.savedRatingStar {
    background-image: url(Images/SavedBeer.gif);
}

The current rating of the article is set in the BindArticle method by setting the Rating Control's CurrentRating property to the Rating property of the Article entity. Remember, the Rating property is one of the immutable properties we added to the Article entity in the custom partial class. The Rating control has a Tag property, which I use to hold the ArticleId being rated.

Using Articlerpt As New ArticleRepository

Dim lArticle As Article = Articlerpt.GetArticleById(ArticleId)
ArticleRating.Tag = ArticleId
ArticleRating.CurrentRating = lArticle.AverageRating

End Using

When a user clicks the Rating control to select a rating for the article, it is quietly posted back to the server using the magical AJAX features of the UpdatePanel. The Rating control's Change event handler is handled by the page and passes the value of the vote to the database by calling the RateArticle member of the ArticleRepository.

Private Sub ArticleRating_Changed(ByVal sender As Object, ByVal e As
AjaxControlToolkit.RatingEventArgs) Handles ArticleRating.Changed
Using Articlerpt As New ArticleRepository
            Articlerpt.RateArticle(e.Tag, e.Value)
End Using
End Sub

Collecting Comments with AJAX

Allowing visitors to comment on articles is a great way to increase community activity around the Beer House. Doing this with AJAX is even cooler. The comments for an article are displayed in a ListView, wrapped in an UpdatePanel. This is nothing new based on what I have already reviewed. Below the comment list is a form, so the visitor can make a comment. This entire form is wrapped in a DIV, called "dComment". There are actually three nested DIVs contained within the fieldset element that wraps around the form. The first DIV contains the Input elements, including a button to submit the comment.

<div id="dComment">
      <fieldset id="commentform">
         <legend>Leave a Comment</legend>
         <div id="dMakeComment">
            <div>
               <label for="txtTitle">
                  Title</label><em>(required)</em></div>
            <div>
               <input id="txtTitle" type="text" value="re: <% =GetTitle()%>"
size="40" /></div>
            <div>
               <label for="txtEmail">
                  Your E-Mail</label>
               <em>(will be kept private)</em></div>
            <div>
               <input id="txtEmail" type="text" /></div>
            <div>
               <label for="txtURL">
                  Your URL</label>
               <em>(optional)</em></div>
            <div>
               <input id="txtURL" type="text" /></div>
            <div>
               <label for="txtComment">
                  Comments</label>
               <em>(required)</em><span id="sCommentReq" style="color: Red;
visibility: hidden;">*</span></div>
            <div>
               <textarea rows="5" cols="25" id="txtComment"></textarea></div>
            <button name="btnStoreComment" id="btnStoreComment"
onclick="StoreComment(); return false;">
               Submit Me!</button>
         </div>
         <div id="dThinking">
            Thinking....</div>
         <div id="dCommentPosted">
            Your Comment has been submitted, check back later to see it
 listed....</div>
      </fieldset>
</div>

The latter two DIVs are initially hidden from view because they will be displayed when the user submits their comment and when the response from the server indicating the comment has been posted is complete. These DIVs are hidden by a combination of CSS and JavaScript. When the page is loaded, it calls the SetPage JavaScript function, defined in the TBHComment.js file. This is added to the Body tag's Onload event in the Page Init event handler. Since the page uses a set of nested master pages a little magic needs to be done by casting the Master property to the correct type where the actual Body tag is located. The Body tag has to be declared as a server-side control by adding runat=server and assigning it an ID, <body runat="server" id="pageBody">.

Private Sub Page_PreInit(ByVal sender As Object, ByVal e As System.EventArgs)
Handles Me.PreInit

        Dim mp As TBHMain = CType(CType(Me.Master, CRMaster).Master, TBHMain)
        mp.pageBodyTag.Attributes.Add("onload", "SetPage();")

End Sub

In the ShowArticle page a ScriptManagerProxy has to be added along with a reference to each the JavaScript file with the client-side code to execute the storing of the comments and the web service:

<asp:ScriptManagerProxy ID="ScriptManagerProxy1" runat="server">
        <Services>
            <asp:ServiceReference Path="~/CommentService.asmx"
InlineScript="true" />
        </Services>
        <Scripts>
            <asp:ScriptReference Path="~/TBHComments.js" />
        </Scripts>
</asp:ScriptManagerProxy>

The submit button calls a JavaScript function, StoreComment, that makes an asynchronous call to a web service that stores the comment:

var dThinking = $get('dThinking'),
var dCommentPosted = $get('dCommentPosted'),

if (dCommentPosted != null) {
    HideElement('dCommentPosted'),
    HideElement('dThinking'),
}

function SetPage() {

    if ($get('dResults') != null) {
        HideElement('dResults'),
    }

}

function StoreComment() {

    HideElement('dMakeComment'),
    ShowElement('dThinking'),

    var ArticleId = $get('ArticleId'),
    var eMail = $get('txtEmail'),
    var Title = $get('txtTitle'),
var Comment = $get('txtComment'),
    var URL = $get('txtURL'),

    if (eMail.value.length > 0 && Comment.value.length > 0 &&
Title.value.length > 0) {

        var objCat = new TheBeerHouse.BLL.Articles.Comment();
        objCat.ArticleId = ArticleId.value;
        objCat.addedByEmail = eMail.value;
        objCat.Title = Title.value
        objCat.Body = Comment.value;
        objCat.CommenterURL = URL.value;
        objCat.AddedByIP = UserIP.value;
        objCat.AddedBy = eMail.value;
        objCat.UpdatedBy = eMail.value;

        //   alert('calling the Service.'),

        TBH_Web35.CommentService.PostComment(objCat, StoreCommentCompleteEvent,
StoreCommentErrorEvent);

        // alert('Called the Service.'),

    } else {
        alert('Just not a valid form.'),
    }

    return false;
}


function StoreCommentCompleteEvent(result, context) {

    HideElement('dThinking'),
    ShowElement('dCommentPosted'),

}

function StoreCommentErrorEvent(result, context) {
    alert('It blew up!'),
    if (null != result) {
        alert(result.get_stackTrace());
    }

}


function ShowElement(ElementName) {
    $get(ElementName).style.display = '';
}

function ShowBlockElement(ElementName) {
    var a = $get(ElementName);
a.style.display = 'block';
    a.style.overflow = 'auto';
    a.style.height = 'auto';
}

function HideElement(ElementName) {
    $get(ElementName).style.display = 'none';
}

function calcTaxes() {
    HideElement('dIntro'),
    ShowBlockElement('dResults'),
    return false;
}

The StoreComment function hides the dMakeComment DIV and displays the dThinking DIV while the comment is being stored. Next, it grabs references to each of the Input elements in the form and sets those values to JavaScript version of the Comment class after it does some basic validation of the data. Remember, the comment has to have a title, the person's e-mail address, and of course the comment. The user's IP address along with the ArticleId are held in Hidden input fields. These fields are accessed by the JavaScript and added to the Comment object before it is posted to the server. Next, the PostComment method of the CommentService is called, passing the Comment and the names of the functions to be called when the operation is complete or an error occurs.

When the web service completes and the comment has been posted, the dCommentPosted DIV is displayed and the dThinking DIV is hidden. This does not completely end the comment process; the comment still needs to be approved by an administrator before it will be displayed to the world.

The CommentService is a normal web service, except that it has a couple of AJAX-related attributes set, ScriptService at the class level and ScriptMethod at the method level. ASP.NET AJAX now knows how to communicate with the web service. The PostComment service itself calls the AddComment method of the CommentRepository and passes the comment provided by the client-side code.

Imports System.Web
Imports System.Web.Services
Imports System.Web.Services.Protocols
Imports System.Web.Script.Services
Imports TheBeerHouse.BLL.Articles


' To allow this Web Service to be called from script, using ASP.NET AJAX,
uncomment the following line.
' <System.Web.Script.Services.ScriptService()> _
<WebService(Namespace:="http://tempuri.org/")> _
<WebServiceBinding(ConformsTo:=WsiProfiles.BasicProfile1_1)> _
<ScriptService()> _
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()> _
Public Class CommentService
    Inherits System.Web.Services.WebService

    <WebMethod()> _
    <ScriptMethod()> _
    Public Function PostComment(ByVal vComment As Comment) As Boolean

        Dim bRet As Boolean = False
vComment.AddedByIP = HttpContext.Current.Request.UserHostAddress

        Using Commentrps As New CommentRepository
            bRet = Commentrps.AddComment(vComment)
        End Using

        Return bRet
    End Function

End Class

The ShowArticle.aspx.vb Code-behind File

When the page loads, it reads the ArticleID parameter from the querystring. This is managed in the ShowArticle.vb code-behind file. The ArticleID parameter must be specified; otherwise, the page will throw an exception because it doesn't know which article to load. Since we are using the site map infrastructure described in Chapter 3, the URLRewriting module handles this for us by exchanging the friendly URL for the direct URL needed to display the article properly.

After an Article object has been loaded for the specified article, you must also confirm that the article is currently published (it is not a future or retired article) and that the current user can read it (if the OnlyForMembers property is true, they have to be logged in). You must check these conditions because a cheating user might try to enter an ArticleID in the URL manually, even if the article isn't listed in the BrowseArticles.aspx page. Of course, this type of hacking is one of the main reasons we are using search engine friendly URLs. If everything is OK, the article's view count is incremented, and Labels and other controls on the page are filled with the article's data:

Private Sub ShowArticle_PreLoad(ByVal sender As Object,
ByVal e As System.EventArgs) Handles Me.PreLoad
        If Not IsPostBack Then
            BindArticle()
        End If
End Sub

Private Sub BindArticle()

        Using Articlerpt As New ArticleRepository

            Dim lArticle As Article = Articlerpt.GetArticleById(ArticleId)
            ArticleRating.Tag = ArticleId
            Title = lArticle.Title
            lblLocation.Visible = (lArticle.Location.Length > 0)
            lblLocation.Text = String.Format(lblLocation.Text, lArticle.Location)
            lblViews.Text = String.Format(lblViews.Text, lArticle.ViewCount)
            ArticleRating.CurrentRating = lArticle.AverageRating

            ArticleDetails = lArticle

            lArticle.ViewCount += 1
            Articlerpt.UpdateArticle(lArticle)

        End Using

End Sub

Some of the value are not bound directly to controls on the page. I did this to demonstrate how to call methods or properties defined in the page directly in the page's markup. The use of Literal and Label controls is not always necessary and can add to the page's overhead and weight. The thinner the better from a performance and search engine placement perspective. So, use this technique to keep your pages a little thinner.

The RSSFeed HttpHandler

As discussed in the "Design" section of this chapter, the RSSFeed handler composes and returns a valid RSS XML document composed of articles in the Beer House site. In the "Design" section of this chapter, you saw the schema of a valid RSS 2.0 document. HttpHandlers are composed of two members, IsReusable and ProcessRequest.

The IsReusable property is a Boolean that indicates to the ASP.NET framework if the request can be used by more than one request at a time. Typically, you want to return false since the content of most handlers is dynamic and each request needs to be sent down a different path. For an RSS feed, it could be set to True because the content being returned is the same and most likely does not change that often.

The ProcessRequest method is automatically called by the framework and accepts a reference to the HttpContext being used to process the request. The HttpContext object provides access to the current HttpRequest and HttpResponse objects, needed to evaluate what was being requested and to send content to the client.

Public ReadOnly Property Response() As System.Web.HttpResponse
        Get
            Return BaseContext.Response
        End Get
End Property

Public ReadOnly Property Request() As System.Web.HttpRequest
        Get
            Return BaseContext.Request
        End Get
End Property

Typically, I create properties to wrap the context, request, and response objects and refactor the worker code to a method that is called by the ProcessRequest method. In the RSSFeed handler The BaseContext property is set to the context parameter. This is then used by the Request and Response properties to return the associated objects to read and write. Finally the CreateRSSFeed method is called to actually produce the RSS feed.

Public Sub ProcessRequest(ByVal context As System.Web.HttpContext)
Implements System.Web.IHttpHandler.ProcessRequest

        BaseContext = context
        CreateRSSFeed()

End Sub

The CreateRSSFeed member reads the list of published articles from the database and produces the RSS feed, an XML document. This is where one of the major differences between VB.NET and C# comes into play, XML Literals (http://msdn.microsoft.com/en-us/library/bb384629.aspx). For the most part, there is little to no difference between the two dominant languages in the .NET world, and by that I mean capabilities that one has the other does not have. For the most part, the languages are differentiated by their syntax. VB has the Dim statement, and C# has a plethora of ;'s and case sensitivity. XML Literals are one clear advantage that VB.NET has over other languages and, as is typical of the Visual Basic experience, it is designed to make development faster.

Simply put, XML Literals allow you to integrate XML directly into your VB.NET code as a natural part of the syntax. For the RSS feed, an XML template can be added directly to the code and logic placed inside the XML to build an XML document.

Private Sub CreateRSSFeed()

  Response.ContentType = "application/xml"

  Using lArticlectx As New ArticleRepository

     Dim Settings As TheBeerHouseSection = Helpers.Settings

     Dim xRss As XDocument = <?xml version="1.0" encoding="utf-8"?>
                       <rss version="2.0" xmlns:geo=
"http://www.w3.org/2003/01/geo/wgs84_pos#">
                          <channel>
                             <title>The Beer House Articles</title>
                             <link>http://www.TheBeerHouseBook.com/</link>
                             <description>RSS Feed containing The Beer House
News Articles.</description>
                             <docs>http://www.rssboard.org/rss-specification</docs>
                             <image>

                                <link>http://www.TheBeerHouseBook.com/</link>
                                <title>The Beer House Articles</title>
                                <url>http://www.TheBeerHouseBook.com
/Images/tbh-logo.png</url>
                             </image>
                             <%= From lArticle In lArticlectx.GetRSSArticles
.AsEnumerable _
                                Select <item>
                                         <title><%= lArticle.Title %></title>
                                         <link><%= SEOFriendlyURL( _
                                                Path.Combine(Settings.Articles
.URLIndicator, lArticle.Title)) %></link>
                                         <description><%= lArticle.Abstract
%></description>
                                      </item> %>
                          </channel>
</rss>

     Response.Write(xRss.ToString)

  End Using

  Response.Flush()
  Response.End()

End Sub

The Response's ContentType property is not set to "text/xml". This is necessary for the browser to correctly recognize the output as an XML document. The XDocument's ToString method is called to get an XML string that is sent to the client by calling the Response.Write method. Finally, the Response.Flush and Response.End methods are called to ensure that any content remaining in the buffer is flushed to the client and to close off any more content from being sent.

An XDocument object, xRSS, is created by setting it to the XML template. Embedded within the XML template is a LINQ query that retrieves Article entities as an Enumerable by calling the AsEnumerable operator. The AsEnumerable operator changes the list of objects to an IEnumerable(Of T), which the XML Literal can traverse through. This is done with a special method in the ArticleRepository class, GetRSSArticles.

Public Function GetRSSArticles() As IEnumerable(Of Article)

            Dim key As String = String.Format(CacheKey)

            If EnableCaching AndAlso Not IsNothing(Cache(key)) Then
                Return CType(Cache(key), List(Of Article))
            End If

            Dim lArticles As IEnumerable(Of Article) = (From lArticle In
Articlesctx.Articles _
                Where lArticle.Active = True And lArticle.Approved = True And _
                lArticle.Listed = True And lArticle.ReleaseDate < Now() And _
                lArticle.ExpireDate > Now() _
                    Order By lArticle.ReleaseDate Descending).AsEnumerable

            If EnableCaching Then
                CacheData(key, lArticles)
            End If

            Return lArticles

End Function

The RSSFeed handler uses the Title and Abstract fields to populate the RSS feed. The <link> element of the feed is built with a helper method, SEOFriendlyURL and the Path.Combine method. Path.Combine takes two strings related to a path, which can be a URL or a UNC file name, and combines them with the proper / or separation. I have found this method to be extremely helpful when composing file paths. The SEOFriendlyURL method accepts a URLIndicator and the article title. It then combines and massages these values to produce a search engine friendly URL that contains the article's title. The result can be seen in Figure 5-15, which shows FireFox consuming the feed.

Figure 5-15

Figure 5.15. Figure 5-15

Also notice the URLs are fully composed and not relative. This is because the RSS feed will be consumed outside the Beer House's website and, thus, need the full URL to be included in the <link> elements.

The RssReader.ascx User Control

The final piece of code for this chapter's module is the RssReader user control. In the ascx file, you define a ListView that displays the title and description of the bound RSS items and makes the title a link pointing to the full article. It also has a header that will be set to the channel's title, a graphical link pointing to the source RSS file, and a link at the bottom that points to a page with more content:

<%@ Control Language="vb" AutoEventWireup="false" CodeBehind="RSSReader.ascx.vb"
    Inherits="TBH_Web35.RSSReader" %>
<asp:ListView runat="server" ID="lvRSSReader" ItemPlaceholderID="itemPlaceHolder">
    <LayoutTemplate>
        <dl runat="server" id="itemPlaceHolder">
        </dl>
    </LayoutTemplate>
    <ItemTemplate>
        <dt><a href="<%#Eval("Url")%>">
            <%#Eval("Title")%></a> </dt>
        <dd>
            <%#Eval("description")%>
            <a href="<%#Eval("Url")%>">...</a></dd>
    </ItemTemplate>
</asp:ListView>

The RssReader.ascx.cs Code-behind File

The first part of the RssReader.ascx.cs code-behind file defines all the custom properties defined in the "Design" section that are used to make this user control a generic RSS reader, and not specific to our site's content and settings:

Public Partial Class RSSReader
    Inherits System.Web.UI.UserControl

    Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs)
Handles Me.Load

        If Not IsPostBack Then
            BindData()
        End If
    End Sub

    Private Sub BindData()

        Trace.Write(Helpers.FormatUrl("TheBeerHouse.rss"))

        Dim rssFeed As XDocument =
XDocument.Load(Helpers.FormatUrl("TheBeerHouse.rss"))

        Dim rssItems = (From rss In rssFeed.<rss>.<channel>.<item> _
                       Select New With {.Title = rss.<title>.Value, _
                        .Url = rss.<link>.Value, _
                        .description = rss.<description>.Value}).Take(5).ToList

        lvRSSReader.DataSource = rssItems
        lvRSSReader.DataBind()

    End Sub
End Class

You don't need to persist the property values in the ControlState here as we did in previous controls because all properties wrap a property of some other server-side control, and it will be that other control's job to take care of persisting the values. All the real work of loading and binding the data is done in BindData method. With XML literals, a simple LINQ statement can be used over the RSS source to retrieve the items and bind them to the ListView.

<mb:RssReader id="RssReader1" runat="server" Title="Latest Articles"
   RssUrl="~/GetArticlesRss.aspx"
   MoreText="More articles..." MoreUrl="~/BrowseArticles.aspx" />

The output is shown in Figure 5-16.

Figure 5-16

Figure 5.16. Figure 5-16

Configuring Security Settings

Many security checks have been implemented programmatically from the page's code-behind. Now you need to edit the Admin folder's web.config file to add the Contributors role to the list of allowed roles for the AddEditArticle.aspx page:

<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
   <system.web>
      <authorization>
         <allow roles="Administrators,Editors" />
         <deny users="*" />
      </authorization>
   </system.web>

   <location path="AddEditArticle.aspx">
      <system.web>
         <authorization>
            <allow roles="Administrators,Editors,Contributors" />
            <deny users="*" />
         </authorization>
      </system.web>
   </location>

   <!-- ManageUsers.aspx and EditUser.aspx pages... -->
</configuration>

Summary

This chapter showed you how to build a complex and articulate module to completely manage the site's articles and announcements. It covered all of the following:

  • An administrative section for managing the data in the database.

  • Pages for browsing the published content.

  • Integration with the built-in membership system to secure the module and track the authors of the articles.

  • A syndication service that publishes a RSS feed of recent content for a specific category, or for every category, by means of an ASP.NET page.

  • A generic user control that consumes any RSS feed. It has been used in this chapter to list the new articles on the home page, but you could also use it in the forums module, and in other situations.

By following along in this chapter, you've seen some of the powerful things you can do with the ListView control, the Entity Framework, and a properly designed business logic layer (BLL).

This system is flexible enough to be utilized in many real-world applications, but you can also consider making some of the following improvements:

  • Support multilevel categories (subcategories management).

  • A search engine could be added to the public section of the modules. Currently, when users want to find a particular article, they have to go through all the content (which could fill several pages in the article list). You could add a Search box that searches for the specified words in the selected category, or in all the categories, and with further options.

  • Extend the ShowArticle.aspx page, or create a separate page that outputs a printer-friendly version of the article, that is, the article without the site's layout (header, menus, footer, left- and right-hand columns). This could be done easily by adding a new stylesheet to the page (when the page is loaded with a PrinterFriendly=1 parameter on the querystring) that hides some DIVs (use the visibility:hidden style).

  • Create a web service that allows other applications to retrieve the list of articles as an alternative to the RSS feed. This could also be used by Contributors to submit new articles, or even by Administrators and Editors to perform their duties. You could use the Microsoft Web Service Extensions (WSE) to make authentication much easier and even to support encryption — otherwise, you'd have to pass credentials in the SOAP headers, and encryption would only be possible by using SSL.

In the next chapter, you'll work on a module for creating, managing, displaying, and archiving opinion polls, to implement a form of user-to-site communication.

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

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