This article describes a set of ASP.NET custom controls that make it quick and easy to add a forum to any web page. The forum code was initially written using VB script and ASP pages and one of the main goals was to mimic the look and feel of the CodeProject forums. Subsequently, the forums have been re-written in C# as ASP.NET custom controls. When the forums are viewed in Tree View mode, the look and feel is very similar to the CodeProject forums. However, in Flat View the forums look have been designed to look more like the forums found at http://www.asp.net/.
The Riverside Internet forums presented here are part of a larger set of ASP.NET custom controls that make up the Riverside Internet WebSolution. The complete WebSolution, as well as containing forums, provides controls for managing folders, documents and general web content. Eventually it is hoped that all of this code will be submitted to CodeProject.
RiversideInternetForums.zip into the directory
- Using IIS, add a new Virtual Directory with alias RiversideInternetForums and specify as the path to the directory that contains the content,
- From within IIS, grant Write access to the avatars directory.
WebSolution.zip to extract the file
WebSolution.sql to your SQL Server to create the database RiversideInternetForums, the tables
WS_Posts, associated indices and stored procedures.
C:\RiversideInternetForums\Web.config so that the application setting "WebSolutionConnectionString" is a valid connection string for the RiversideInternetForums database.
- You will have to add a new user to the RiversideInternetForums database, which is permitted in the database roles: public and db_owner. When using a Trusted Connection, the name of this user will take the form
- Start your browser and open the virtual directory RiversideInternetForums, for example http://localhost/riversideinternetforums/, to see the results. You should be presented with an empty forum.
- Click on Join to setup a new user. Then start posting!
A warning: Although not described in this article, the Riverside Internet WebSolution is designed to support multiple websites from a single database/codebase installation. Multiple websites are supported by associating a web identifier (
WebID) to each website (
WebDomain). These associations are stored in the database table
WS_Webs. By default, the WebSolution.sql script inserts the web identifier 1 for the website "localhost". However, if your IIS server is not referred to by "localhost", then you will need to change the value "localhost" to reflect the name of your IIS server. Similarly, if your website can be accessed through a URL such as http://www.riversideinternet.com/, then a row must be inserted in to
WS_Webs that has
WebID equal to 1 and
WebDomain equal to 'riversideinternet.com'.
WebDomain is always lowercase and is set to the host name of HTTP requests with the initial www dropped).
WS_Webs.Folder can be left NULL.
The first thing to note is how simple it is to add a forum to a web page. All that is required is the following two lines (see
<%@ Register TagPrefix="WS" Namespace="RiversideInternet.WebSolution"
<WS:Forum ForumID="1" Runat="server" id="_forum" />
The control that provides links for Join, Logon, My Settings and Logoff at the top of
default.aspx is created using the following code:
BorderStyle="None" Runat="server" id= "_loginControl" />
Certain actions, such as starting a new thread or posting a reply, require a user to be logged on. If a user is not currently logged on, performing one of these actions will cause the user to be redirected to a login page. The page redirected to is determined by the application setting "WebSolutionUserManagementURL" found in
Web.config. This page should contain the user management control:
<WS:UserManagement Runat="server" id= "_userManagement" />
The forums use "Forms" Authentication to determine whether a user is logged on or off. Therefore the authentication mode found in
Web.config should be set to "Forms".
Other application settings found in
Web.config are "WebSolutionAvatarsURL", which is the web address where avatars can be found. In the
Web.config supplied, this is set to "/RiversideInternetForums/avatars/". The application setting "WebSolutionAvatarsPath" should point to the physical location on disc where avatars are located. In the example
Web.config provided, this is set to "C:\RiversideInternetForums\avatars\". Finally, the application setting "WebSolutionImagesURL" must point to the web address where forum images are found. In the example code, this is set to "/RiversideInternetForums/images/".
The appearance of the forum, user management and other controls is determined by the style sheet
websolution.css. This file can be customised to change the appearance of the various controls.
In the example above, only one page is required to display the forum - namely,
default.aspx. The custom control
RiversideInternet.WebSolution.Forum detemines what HTML should be rendered by looking at the query string. The query string variable
threadid is used to identify a thread to view. The query string variable
postid may also be used to specify a post within a thread. If neither
threadid is specified, the forum code knows it should display a paged list of forum threads.
25 threads are displayed per page with information such as thread title, thread starter, number of replies, number of views and the name of the last poster all present. A thread with more than 30 posts is indicated by the image of a folder on fire. Threads are ordered descending by the time of the last post. Therefore if someone replies to a thread that is some way down the list, it will be elevated to the top of the list.
postid are specified in the query string, the forum code knows it should display a particular thread. Instead of rendering the paged list of forum threads, an individual thread is displayed. A thread can be displayed in flat view mode or tree view mode. In flat view mode, a thread has it's posts displayed in order oldest to newest and the body of each post is visible. In tree view mode, a thread is displayed as a hierarchy of posts and looks similar to a thread on the CodeProject forums. In both modes, 30 posts are displayed per page. A cookie is used to store whether the user is in tree view or flat view mode and this setting affects all forums.
The query string variable
forumaction is used when starting a new thread, editing a post, replying to a post, quoting a post or performing a search. With the exception of search, every one of these actions requires a user to be logged on. If no user is logged on, the web browser will be redirected to a login page. After a successful logon, the browser will be automatically redirected back to the previous page. When
forumaction has the value "new" (and no
postid is specified), the forum control knows that it should render a form for starting a new thread. When
forumaction has the value "reply" (and
postid is specified), the forum control renders a form for posting a reply. The "quote" action is similar to the "reply" action, except the post being replied to is encompassed by [QUOTE] tags and some additional text is included identifying who posted the message being quoted. This is most useful when a forum is in flat mode, as it can sometimes be difficult to identify the post a user is replying to. The action "edit" is used when a user is editing a post. A user may only edit posts that they have authored.
forumaction equals "search", the forum code knows to perform a search. In this case, an additional query string variable called
searchterms is used which contains the terms used to search the forum. Posts that match the search critieria are displayed in a paged list, order by posted date descending. In order for a post to be identified by a search, each search term specified must be found somewhere in the body of the post.
User Management Overview
In the example above, only one page is required to display user management information - namely,
usermanagement.aspx. The custom control
RiversideInternet.WebSolution.UserManagement determines what HTML should be rendered by looking at the query string. The action performed depends on the query string variable
useraction, which can take one of four possible values: "login", "logoff", "join" and "settings".
useraction has the value "login", the user management control renders a login form that prompts the user for an e-mail address and password. Ticking the remember me check box causes a durable cookie (one that is saved across browser sessions) to be written to the client and this will have the effect of logging the user on automatically for subsequent browser sessions. If
useraction has the value "logoff", the user management code renders a simple HTML page which contains a logoff button. When this button is pressed, the currently logged on user is logged off.
useraction values "join" and "settings" are used to sign a user up or modify the logged on user's settings. In each case, the user management control renders a form displaying Alias, e-mail, password, confirm password and avatar text boxes. A user's alias is the name by which he or she is known on the forums. A user's e-mail address is used in conjunction with a password in order to login. It may also be used when a user has submitted a post with the "Notify by e-mail" option ticked. When such a post is replied to, a notification e-mail is sent to the author of that post. Finally, the avatar text box is used to upload an image to the web server. Avatars are images that are displayed underneath user information towards the left hand side of every post. If a user has not uploaded an avatar, no image is displayed alongside posts by that user. Avatars must be no larger than 150 by 150 pixels, must be less than 100K in size and must be in the format GIF, JPEG or PNG. The forums screenshot, shown above, displays posts by Nick's Mum, Pud and The Mole. Their avatars, a dog wearing glasses, a Triumph Dolomite Sprint and a monkey, are all visible.
Role-Based Security With Forms Authentication
As has already been stated, the forums use "Forms" Authentication to determine whether a user is logged on or off. Therefore the authentication mode found in
Web.config should be set to "Forms". The forums use role-based authentication to determine what operations a user can perform. The roles a user can perform are stored in a comma separated list in the database field
WS_Users.Roles must be administered using raw SQL or Enterprise Manager. Users with "ForumAdmin" role, may edit the posts of other users as well as enter sticky (pinned) threads. When users with "ForumAdmin" role post, an additional drop down list is visible that allows the admin user to specify whether a thread is sticky and the duration that it should stick.
Role-based security has been implemented using code written by Heath Stewart. Thanks, Heath! For more information on role-based security, please read Heath Stewart's article.
Functionality Controlled By Query String
In order to understand how the query string controls functionality, it is necessary to understand three of the commonly overridden methods of
CreateChildControls function is often overridden in composite controls (such as the forum and user management controls described above) in order to create child controls in preparation for postback or rendering.
OnPreRender is called to implement any work that is required by a control before rendering, for example getting data from a database. Finally, the
Render method is used to write markup text (HTML) to the output stream.
RiversideInternet.WebSolution controls, such as the forum and user management controls, derive from
WebSolutionControl, in turn, derives from
System.Web.UI.WebControls.WebControl which derives from
System.Web.UI.Control. What is important is that in
WebSolutionControl, three functions are overridden:
public class WebSolutionControl : WebControl, INamingContainer
private WebSolutionObject _webSolutionObject;
protected virtual void CreateObject()
protected override void CreateChildControls()
if (_webSolutionObject != null)
protected override void OnPreRender(System.EventArgs e)
if (_webSolutionObject != null)
protected override void Render(System.Web.UI.HtmlTextWriter writer)
if (_webSolutionObject != null)
CreateChildControls, called before
Render, calls the virtual function
CreateObject. The task of
CreateObject is to create a
WebSolutionObject and store it's reference in the member variable
WebSolutionObject class contains some commonly used functions and three public virtual functions
Render. After the call to
CreateObject, the function
_webSolutionObject.CreateChildControls is called. Following
CreateChildControls are calls to
Render and these functions simply delegate out the work by calling
Forum control overrides
CreateObject to create either a
ForumForm or a
ForumSearch object, depending on the query string. As these objects are derived from
WebSolutionObject, they can (and do) implement
Render functions. These four classes are responsible for implementing the four areas of functionality described in the Forum Overview section above.
UserManagement control overrides
CreateObject to create either a
UserSettings object depending on the query string. These three classes are reponsible for implementing the three areas of functionality described in the User Management Overview section above.
It should also be noted that
INamingContainer is a marker interface that does not have any methods but causes the page to create a new naming scope under each
WebSolutionControl. When this interface is implemented, any child controls contained are guaranteed to have identifiers (represented by the
UniqueID property) that are truly unique on the page.
Database Access Layer
All of the controls implemented in the Riverside Internet WebSolution use a middle-tier component to provide communication between the WebSolution controls and the SQL Server database. The middle-tier component is comprised of the classes
UserDB. Only these classes use
System.Data.SqlClient and information is passed back to WebSolution controls via simple helper classes.
UserDB has a static function called
GetUser that takes a user identifier (an integer) as an input parameter and returns information about the specified user. Information is returned in a
User object that has properties
WebID that correspond to each of the fields found in the
Other classes used to communicate between the middle-tier database access layer and the server controls include:
ForumPost. When viewing the paged list of forum threads, the information required to render each row in the list is obtained from a
ForumThreadInfo object. In fact, the function
ForumDB.GetThreads returns a
ForumThreadInfoCollection, which is an ArrayList of
ForumThreadInfo, and this collection is used to render the paged list of forum threads. Forum search results are coded in the same way. However, in this case
ForumDB.GetForumSearchResults returns a
ForumDB.GetThreads calls the stored procedure
WS_GetThreads, which takes three input parameters: a forum identifier (
@ForumID int), a page size (
@PageSize int) and a page index (
@PageIndex int). The query string variable
threadspage (the page index) is used to determine which page of threads a user is viewing. With a page size of 25 and
threadspage equal to 1, the first 25 threads are visible. When
threadspage equals 2, threads 26-50 are visible and so on. The important thing to note is that
WS_GetThreads does not return all forum threads to the web server. Rather, it returns a page of threads depending on
@PageIndex . In this way network traffic is reduced and the hard work is done on the database server where it is usually possible to get the best performance for this kind of operation. Furthermore, paging results in this way greatly simplifies the C# code.
Paging is achieved by creating a temporary table,
WS_GetThreads which has an identity field,
IndexID, and a
ThreadID field into which all thread identifiers belonging to a particular forum are inserted. The stored procedure
WS_GetThreads then SELECTs out and returns only a subset of threads stored in
#PageIndex depending on
CREATE PROCEDURE WS_GetThreads
DECLARE @PageLowerBound int
DECLARE @PageUpperBound int
SET @PageLowerBound = @PageSize * @PageIndex
SET @PageUpperBound = @PageLowerBound + @PageSize + 1
CREATE TABLE #PageIndex
IndexID int IDENTITY (1, 1) NOT NULL,
INSERT INTO #PageIndex (ThreadID)
ForumID = @ForumID
WS_Threads, ..., #PageIndex PageIndex
WS_Threads.ThreadID = PageIndex.ThreadID AND
PageIndex.IndexID > @PageLowerBound AND
PageIndex.IndexID < @PageUpperBound AND
The original idea for the paging technique described above, found in stored procedures
WS_GetForumSearchResults, came from the http://www.asp.net/ forums.
Finally, a few words of explanation about the
WS_Posts tables, which are at the heart of the Forum control. When a new thread is started, a row is inserted into both
WS_Posts.PostID is an identity field and so is automatically assigned a unique value when a row is inserted into
WS_Posts. The row inserted into
ThreadID set to this automatically generated value (
WS_Posts.PostID). As replies to a thread are posted, rows are inserted into
ParentPostID field is set to the identifier of the post replied to. When a new thread is started, the row inserted into
ParentPostID equal to 0 - that is, it has no parent. For every reply inserted into WS_Posts,
WS_Threads.Replies is incremented by 1 and
WS_Threads.LastPostedPostID is updated to reflect the post which has most recently been made to a particular thread.
WS_Posts has fields
FlatSortOrder that are maintained so that the stored procedure
WS_GetThread can quickly obtain a paged list of thread posts.
TreeSortOrder are used when viewing a thread in tree view mode, while
FlatSortOrder is used when viewing a thread in flat view mode.
PostLevel field indicates at what depth in the tree view hiearchy a post sits. When a thread is started, a row is inserted into
PostLevel 0. Replies to this post will have
PostLevel 1. Replies to any of these posts will have
PostLevel 2. On screen,
PostLevel indicates the size of horizontal ident from the left hand edge before a post is rendered. The
TreeSortOrder field maintains the order in which posts are displayed in tree view mode. When a new thread is started a row is inserted into
TreeSortOrder equal to 0 (call this Post 1). The first reply to this post will have
TreeSortOrder 1 (Post 2). Replying to Post 1 again will cause another row to be inserted into
TreeSortOrder 2 (Post 3). However, if a reply is posted to Post 2 (call this Post 4), this post will assume
TreeSortOrder 2 with Post 3 shifted downwards to
FlatSortOrder field maintains the order in which posts are displayed in flat view mode. In flat view mode, posts are simply displayed in order oldest to newest. Therefore, when a new thread is started the first post has
FlatSortOrder 0. Subsequent replies have
FlatSortOrder equal to 1, 2, 3, etc.
FlatSortOrder are automatically updated in the stored procedure
WS_AddPost, which is based on a similar stored procedure found in the http://www.asp.net/ forums.
Interesting Bits And Pieces
Here are a few other interesting bits and pieces that are worth talking about...
Uploading And Validating Avatars
UserManagement control, when in "join" or "settings" mode, facilitates the upload of an avatar from the client machine to the web server. The avatar must be no larger than 150 by 150 pixels, less than or equal to 100K in size and be of the type GIF, JPEG or PNG. In ASP.NET it is easy to upload files to a web server, using the
System.Web.UI.HtmlControls.HtmlInputFile control. However, in order for this to work the Web Form (in the example code provided, usermanagement.aspx) must have the property enctype set to "multipart/form-data". The code for uploading the avatar can be found in the
UserSettings class, while the code for validating the properties of the uploaded file (dimensions, size, type) can be found in the
ValidAvatar class, which is derived from
HtmlInputFile control is used in
ValidAvatar to validate the size and type, while the
System.Drawing.Image class is used to validate image dimensions.
Sending E-mail From An ASP.NET Application
EmailReplyNotification in the
ForumForm class illustrates how to use the
System.Web.Mail.SmtpMail classes for sending e-mail.
Case Insensitive String Replace
ForumText class is used to perform forum string manipulations such as replacing text like :), ;), :( with the appropriate emoticons. Of particular interest is the function
FormatDisableHtml, which replaces potentially malicious HTML such as <script with <script. As HTML is case insensitive, all upper and lower case combinations of <script (for example: <SCRIPT, <script and <ScRiPt) must be replaced by <script. In order to do this, a case insensitive string replace is required and unfortunately the replace method of the
System.String class is case sensitive. A quick search on Google revealed this article by Brian Bilbro, from which the code in
WebSolutionUtils.ReplaceCaseInsensitive is based.
Things Still To Do
The Riverside Internet WebSolution is a work in progress and there are still a great many things to code and improve. Forum things still to do include:
- Administrator rights for deleting posts without having to use raw SQL statements!
- Time zone management.
- Improve user management control.
- Improve the way look and feel is customised by utilising
- Improve design time support within Visual Studio .NET.
- Improve error handling.
- Implement posts that can't be replied to.
- Improve searching.
- etc. etc.
Version 1 - 9th June 2003
- This is the first version uploaded to CodeProject.
Version 2 - 13th August 2003
- Last Post column in thread list now displays date and time of last post and provides a link to navigate straight to the last post.
- Role based forms authentication introduced. Users with role "ForumAdmin", can modify other users posts and post pinned (sticky) threads.
- Dynamic tree view mode added.
- QueryString simplified by removing threadpage and threadid parameters. Instead, a single postid parameter is used.
WS_Users.UseAvatar field now obsolete, so removed.
WS_Threads.IsPinned field now obsolete, so removed.
WS_Users.Roles field added for role based authentication.
- Fixed e-mailing of reply notification bugs.
- Fixed bugs where malicious HTML can be executed when displaying Alias and post subject in search list.
- Fixed bug that replaces the word "select" with "<select".
- Fixed bug that meant confirm password was need not always required on user settings and join pages.
- Previously, updating a user's settings may have caused the user's avatar to be deleted. This bug has been fixed.
- Update database from Version 1 to Version 2 with the file update1to2.sql found in WebSolution.zip (download above).