Before looking at the design, let's consider a more accurate list of features to be implemented:
Support for multiple categories, or subforums, that are more or less specific to a single topic/ argument. Subforums are identified by name and description, and optionally by an image.
Forums must support moderation: When a forum is moderated (this is a forum-level option), all messages posted by anyone except power users (administrators, editors, and moderators) are not immediately visible on the forum, but must be approved by a member of one of the power user roles first. This is a useful option to ensure that posts are pertinent, non-offensive, and comply with the forum's policy. However, this also places a bigger burden on the power users because posts have to be approved often (at least several times a day, even on weekends), or users will lose interest. Because of the timeliness needed for moderation, most forums are not moderated, but they are checked at least once a day to ensure that the policy has not been violated (with no particular need to check on weekends).
The list of threads for a subforum, and the list of posts for a thread, must be paginable. In addition, the list of threads must be sortable by the last posting date, or the number of replies or views, in ascending or descending order. Sort options are very helpful if there are a lot of messages.
Posting is only permitted by registered members, whereas browsing is allowed by everybody. An extension of the forum implemented in this chapter may include more options to specify that browsing also requires login, or that posting is allowed by anonymous users.
Users will be able to format their messages with a limited, and safe, set of HTML tags. This will be done by means of the FCKeditor control already used in Chapter 5, with a reduced toolbar.
While creating a new thread, a user must be able to immediately close the thread so that other users cannot reply. If replies are allowed, they can later be disabled (and thus the thread closed) only by administrators, editors, and moderators.
Users can modify their own posts anytime, but the operation must be logged in the message itself (a simple note in the message saying that it was edited will avoid confusion if another user remembers seeing something in a message but the next time they look it's gone).
Users can have an avatar image associated with their account, which is displayed on every post they make. This helps them create a virtual, digital identity among other users of the forum. Users can also define a signature to be automatically added at the end of each post, so that it doesn't need to be copied and pasted every time. Signatures often includes a special greeting, motto, old saying, or any other quote taken from movies, from famous people, or coined by the member herself. Sometimes it will contain a URL of that person's own site — this is normally OK, but you probably don't want any kind of advertising in this manner (e.g., www.BuyMyProduct.com).
The messages posted by users are counted, and the count is displayed together with the user's avatar, on each of the user's messages. This count is a form of recognition, and it lets other users know that this person might be more knowledgeable, or at least that they've hung around in the forums a lot (it tends to lend more credibility). In addition, the forum system supports three special user levels; these are descriptions tied to the number of posts each user has made. The number of posts needed to advance to the next level can be configured, just like the descriptions.
Full support for RSS 2.0 feeds should allow users to track new messages in an RSS aggregator program (such as the free SharpReader, or RSS Bandit). There will be a flexible syndication system that provides distinct feeds to specific subforums, or all forums, and it will sort posts in different ways. This enables users to get a feed with the 10 newest threads posted into any forum, or with the 10 most popular threads (if sorted by number of replies). The feeds will be consumed by the generic RSS Reader control already developed in Chapter 5.
Administrators, editors, and moderators can edit and delete any post. Additionally, they can move an entire thread to a different forum, which is helpful for ensuring that all threads are published in the appropriate place.
Important |
Remember that you need some kind of policy statement somewhere in the forum pages that tells users what the rules are. This is usually needed for legal reasons in case a nasty, hateful, or untruthful message is posted and not caught quickly — just some kind of disclaimer to protect the site owners/administrators from lawsuits. |
To help you understand the database design of this module, we'll compare it to the design that was implemented in the first edition of this book, examining the changes (but it's OK if you didn't read the first edition — you will still see some value in this comparison). Figure 8-1 represents the first edition's UML diagram of all the tables.
With six tables for a single module, this made the forums system the most complex module of the book. In this edition, we'll simplify the design significantly. We'll also add some new features, but without removing anything important. In Figure 8-1 you can see that we had a two-level organization for message threads: categories and then subforums. A simpler design with multiple subforums would be entirely adequate in most situations. We also had two separate tables for storing the first message of threads (the Forums_Threads table) and the replies (the Forums_Replies table): These two tables do not differ for many fields, as they both have a primary key field and the Message, MemberID, MemberIP, and AddedDate fields. The first message of a thread and its replies are all forum posts, and dividing them in two separate tables only complicates things, i.e., it leads to longer queries and is more difficult to read and maintain. In the new design there is just a single table for all messages; the few fields that only make sense for one type of message will simply have a default value for the other type. Additionally, we had threads and replies linked to a record of the Forums_Members table, which contained a profile of the user, with her signature, avatar, and other information, and this was linked to a record in the Accounts_Users table, which contained the membership information (username, password, etc., of all user accounts). Yes, we had two redundant places to hold user data, and it was confusing and caused problems when they became out of sync. In the new version we'll consolidate all membership and profile data, and it will be stored and managed automatically by the ASP.NET provider classes!
To recap, the new forums system will just have one table that stores subforum information, and another one that stores all messages. Figure 8-2 represents the tables as shown by the Database Diagram Editor window of VS 2005's Server Explorer tool (which is similar to what Enterprise Manager would show in SQL Server 2000 with VS 2003 — now you can stay inside VS most of the time).
The tbh_Forums table is similar to the tbh_Categories table used in Chapter 5 for the articles module, with the addition of the Moderated column, which indicates whether the messages posted by normal users must be approved before they become visible in the forums' pages. The tbh_Posts table contains the following columns (the usual AddedDate and AddedBy fields aren't shown here):
PostID: The primary key
AddedByIP: The IP of the user who authored the message — used for auditing purposes. Remember that you may become partially responsible for what users write (this also depends on the laws of your country). You should try to log information about the user who posted a message (such as the date/time and IP address) so you can provide this to legal authorities in the unlikely event that it might be needed.
ForumID: The foreign key to a parent forum
ParentPostID: An integer referencing another record in the same table, which is the first message of a thread. When this field contains 0, it means that the post has no parent post; therefore, this is a thread post. Otherwise, this is a reply to an existent thread.
Title: The title of the post. Reply posts also have a title; it will usually be "Re: {thread title here}", but it's not absolutely necessary and the user will be free to change it while posting a new reply.
Body: The body of the post, in HTML format (only limited HTML tags are allowed)
Approved: A Boolean value indicating whether the post has been approved by a power user (administrators, editors, and moderators), and visible on the end-user pages. If the parent forum is not moderated, this field is automatically set to 1 when the post is created.
Closed: This field is only used for thread posts, and is a Boolean value indicating whether the thread is closed and no more replies can be added. The user will be able to specify this option only while creating the thread. Once a thread has been created, only power users can close the thread.
ViewCount: An integer indicating the number of times a thread has been read. If the record represents a reply, this field will contain 0.
ReplyCount: The number of replies for the thread post. If the record represents a reply, this field will contain 0.
LastPostBy: The name of the member who submitted the last post to this thread. As long as there are no replies, the field contains the name of the member who created the thread, which is also the name stored in the record's AddedBy field.
LastPostDate: The date and time of the last post to this thread. As long as there are no replies, the field contains the date and time when the thread was created, which is also the value stored in the record's AddedDate field.
In the case of ParentPostID, the replies will always link to the first post of the thread, and not to another reply. Therefore, the proposed structure does not support threaded discussions, such as those in Internet newsgroups. Instead, posts of non-threaded discussions will be shown to the reader, sorted by creation date, from the oldest to the newest, so that they are read in chronological order. Both of these two types of forum systems, threaded or not, have their pros and cons. Threaded discussions make it easier to follow replies to previous posts, but non-threaded discussions make it easier to follow the discussion with the correct temporal order (time-sequenced). To make it easier for the reader to follow the discussion, non-threaded discussions usually allow users to quote a previous reply, even if the referenced reply is a number of posts prior to that one. In my research, non-threaded discussions are more widely used, and easier to develop, so we'll use them for our sample site. If you want to modify the forum system to support threaded discussions, you'll be able to do that without modifying the DB; you just need to set the post's ParentPostID to the appropriate value.
Our first edition used SQL aggregate functions COUNT and MAX to dynamically compute the thread's message count values instead of storing them in the database. This was a serious problem in terms of performance because it took a long time to execute this SQL, and it ran often. It will save a lot of time if you store the values directly in the thread post's record, and update them when a new reply is added, as we'll do in this book.
The following table contains the list of stored procedures for the typical CRUD operations on the two database tables, plus some special updates to handle approving posts, moving threads, incrementing the view count, and so on.
Property |
Description |
---|---|
tbh_Forums_ApprovePost |
Sets the Approved field to 1 for the specified post, indicating that the post will be visible to all users. It also increments the parent post's ReplyCount field by one, and sets the parent post's LastPostBy and LastPostDate fields to the corresponding values of the post being approved now. |
tbh_Forums_CloseThread |
Sets the Closed field of the specified thread to 1, meaning that the thread will not allow any further replies |
tbh_Forums_DeleteForum |
Deletes the specified forum, and all its child threads and posts |
tbh_Forums_DeletePost |
Deletes the specified post. If this post is the first message of a thread, this also deletes all its replies. |
tbh_Forums_GetForumByID |
Returns all details of the specified forum |
tbh_Forums_GetForums |
Returns all details of all forums |
tbh_Forums_GetPostBody |
Retrieves the body of the specified post |
tbh_Forums_GetPostByID |
Retrieves all details of the specified post |
tbh_Forums_GetPostCount ByThread |
Returns the number of posts for the specified thread |
tbh_Forums_GetThreadByID |
Returns all details for all posts in the specified thread |
tbh_Forums_GetThreadCount |
Returns the number of threads from all forums |
tbh_Forums_GetThreadCount ByForum |
Returns the number of threads from a specific forum |
tbh_Forums_GetUnapproved Posts |
Retrieves all details of all unapproved posts, sorted by type (thread posts first, and then reply posts) and then from the oldest to the newest. The logic behind these sorting options is that it's more important to approve new threads before new replies, and then it's more important to approve posts starting from the oldest because they have been waiting longer for approval. |
tbh_Forums_Increment ViewCount |
Increments the ViewCount field of the specified thread post by one |
tbh_Forums_InsertForum |
Creates a new forum with the specified details, and returns its auto-generated ID |
tbh_Forums_InsertPost |
Creates a new post and returns its auto-generated ID. If the post has its ParentPostID field set to 0, it means this is a thread post, and its LastPostBy and LastPostDate fields will be set equal to the AddedBy and AddedDate values specified. If the post represents a reply instead, and it is approved, then its AddedBy and AddedDate fields will be used to update its parent post's Last-PostBy and LastPostDate fields, and the parent post's Reply-Count field will be incremented by one. |
tbh_Forums_MoveThread |
Moves an entire thread to a different forum, by updating the ForumID field of the thread's posts to the specified destination forum's ID |
tbh_Forums_UpdateForum |
Updates some details of the specified forum |
tbh_Forums_UpdatePost |
Updates the title and body of the specified post. All other details are not editable by this procedure, but are editable by other stored procedures such as tbh_Forums_CloseThread, tbh_Forums_MoveThread, tbh_Forums_ApprovePost, tbh_Forums_IncrementViewCount, etc. |
There are two notable queries missing from the preceding list. One is tbh_Forums_GetThreads, which returns the details of a page of threads. The other is tbh_Forums_GetThreadsByForum, which should return the details of a page of threads for a specific forum. These queries are not defined as stored procedures because the list of threads must support different ordering (LastPostDate, ReplyCount, and ViewCount), and unfortunately we can't parameterize the ORDER BY clause used in the stored procedure together with the ROW_NUMBER()... OVER statement. The simplest solution to this problem is to dynamically build and execute these queries from the data access layer, according to the input parameters. As explained in Chapter 3, if you use parameters in the SQL query, SQL Server will be able to cache the execution plan and you'll be shielded against SQL injection attacks, even if you're not using a stored procedure.
Note |
Retrieving the list of threads present in all forums is only useful for sitewide RSS feeds that list the last "n" threads created, or the "n" most active threads (threads with the most replies). |
The configuration settings of the forums module are defined in a <forums> element within the <theBeerHouse> section of the web.config file. The class that maps the settings and exposes them is ForumsElement, which defines the following properties:
Property |
Description |
---|---|
ProviderType |
The full name (namespace plus class name) of the concrete provider class that implements the data access code for a specific data store |
Connection StringName |
The name of the entry in web.config's new <connectionStrings> section containing the connection string for this module's database |
EnableCaching |
A Boolean value indicating whether caching is enabled |
CacheDuration |
The number of seconds for which the data is cached if there aren't any inserts, deletes, or updates that invalidate the cache |
ThreadsPageSize |
The number of threads listed per page when browsing the threads of a subforum |
PostsPageSize |
The number of posts listed per page when reading a thread |
RssItems |
The number of threads included in the RSS feeds |
HotThreadPosts |
The number of posts that make a thread hot. Hot threads will be rendered with a special icon to be distinguished from the others. |
BronzePosterPosts |
The number of posts that the user must reach to earn the status description defined by BronzePosterDescription |
BronzePoster Description |
The title that the user earns after reaching the number of posts defined by BronzePosterPosts |
SilverPosterPosts |
The number of posts that the user must reach to earn the status description defined by SilverPosterDescription |
SilverPoster Description |
The title that the user earns after reaching the number of posts defined by SilverPosterPosts |
GoldPosterPosts |
The number of posts that the user must reach to earn the status description defined by GoldPosterDescription |
GoldPoster Description |
The title that the user earns after reaching the number of posts defined by GoldPosterPosts |
As usual, the DAL consists of a number of entity classes that wrap data from the database tables (the ForumDetails and PostDetails classes), an abstract class defining a number of virtual methods to manipulate the data and a few helper methods (ForumsProvider), and finally a concrete class that inherits from the abstract class and implements its virtual methods with the code to call the respective stored procedures. Figure 8-3 shows the UML diagram of all these classes and their relationships.
Besides GetThreads, all other DAL methods simply wrap a stored procedure, so they don't require any further explanation. As explained earlier, GetThreads executes the dynamically constructed SQL queries in order to support different sorting options according to the input parameters.
Like the DAL, the BLL for this module is very similar to the other BLLs used in this book. There's a BaseForum class that contains the usual ID, AddedDate, and AddedBy properties common to both the tbh_Forums and the tbh_Posts tables, a Settings property, which returns an instance of the ForumsElement class, which wraps all forums' settings read from web.config, and finally a CacheData method that caches the input data according to the forums' settings. The derived classes are Forum and Post. Forum is almost identical to the Category class designed and implemented in Chapter 5, with the addition of the Moderated property. Post represents both threads (the first post of a thread) and replies, and it has properties that wrap all data coming from the PostDetails DAL class, plus instance methods such as Update, Delete, Approve, MoveThread, and CloseThread that manipulate the data contained in the current object. These instance methods simply forward the call to static methods that in turn call the respective DAL methods and add some logic to store and purge data from the cache, add data validation, and so on. The methods for retrieving a single post, the list of posts for a given thread, or the list of threads for a given forum are also implemented as static methods. Figure 8-4 shows the UML diagram of the BLL.
The last thing we need to define are the UI pages and user controls that enable the user to browse forums and threads, post new messages, and administer the forum's content. Following is a list of the user interface pieces that we'll develop shortly in the "Solution" section:
~/Admin/ManageForums.aspx: Adds, updates, and deletes forums
~/Admin/ManageUnapprovedPosts.aspx: Lists all unapproved posts (first thread posts, and then replies, all sorted from the oldest to the newest), shows the entire content of a selected post, and approves or deletes it
~/Admin/MoveThread.aspx: Moves a thread (i.e., the thread post and all its replies) to another forum
~/ShowForums.aspx: Shows the list of all subforums, with their title, description, and image. Clicking on the forum's title will bring the user to another page showing the list of threads for that forum. For each forum, this also provides a link to its RSS feed, which returns the last "n" threads of that forum (where "n" is specified in web.config).
~/BrowseThreads.aspx: Browses a forum's threads, page by page. The grid that lists the threads shows the thread's title, the number of times it was read, the number of replies, the author of the last post, and when the last post was created. Power users also see special links to delete, close, or move the thread. The results can be sorted by date, reply count, or view count.
~/ShowThread.aspx: Shows all posts of a thread, in a paginated grid. For each post, it shows the title, body, author's signature, submission date and time, author's name, avatar image, and status description. Power users also see links to delete or edit any post, and a link to close the thread to stop replies. Normal members only see links to edit their own posts.
~/AddEditPost.aspx: Creates a new thread, posts a new reply, or edits an existing message, according to the parameters on the querystring
~/GetThreadsRss.aspx: Returns an RSS feed of the forum's content. According to the querystring parameters, it can return the feed for a specific subforum or include threads from any subforum, and supports various sorting options. This can retrieve a feed for the sitewide threads (if sorting by date) or for the most active threads (if sorting by reply count).
~/Controls/UserProfile.ascx: This control already exists, as it was developed in Chapter 4 while implementing the membership and profiling system. However, you must extend it here to support the Avatar image and Signature profile properties.