Build Community with a Discussion Forum: Part II

Develop Main Forum Pages and Administrative Pages

CodeTalk

LANGUAGES: C#

ASP.NET VERSIONS: 3.5

 

Build Community with a Discussion Forum: Part II

Develop Main Forum Pages and Administrative Pages

 

By Bipin Joshi

 

In Part I of this article we discussed the overall functional requirements and the logic of a discussion forum application. We also created the necessary database tables, as well as AJAX-enabled log-in and registration pages. In addition, we developed a Web service with helper Web methods to be consumed from client-side script. Now it s time to develop the Web forms that display forums and threads. Any forum application needs some facility for administering and moderating forum posts and replies. We ll also develop administrative Web forms.

 

Displaying a List of Forums

When a user logs in to the system, the default page of the Web site should display a list of available forums (see Figure 1). Notice how the balloon with information about a forum is displayed. The balloon displays the description of the forum along with total post and thread information. The balloon is displayed using AJAX Web service calls.

 


Figure 1: Displaying a list of available forums.

 

To develop the default Web form, drag and drop a ScriptManagerProxy control on the Web form. Recollect that we ve placed the ScriptManager control on the master page, and a Web form can have one, and only one, instance of ScriptManager. The ScriptManager control from the master page doesn t include a reference to the Web service we created last time. This is because the Web service is not needed in all the Web forms. The ScriptManagerProxy control will allow us to refer the Web service required by the default page.

 

To add a reference to the Web service, locate the Services collection of the ScriptManagerProxy control in the property window and add a reference to the WebService.asmx file (see Figure 2). Now drag and drop a SQL data source control and configure it to select all the records from the ForumCategories table (see Figure 3). The balloon tooltip displayed in the browser is actually a Panel control with a Label placed inside it. The markup of this Panel control is shown in Figure 4. This Panel and Label are used by the PopupControlExtender control from the AJAX Control Toolkit to display the balloon. You ll see later how the PopupControlExtender is used.

 


Figure 2: Adding a reference to WebService.asmx.

 


Figure 3: Configuring SQL data source to select records from the ForumCategories table.

 

BackColor="FloralWhite" BorderColor="Gainsboro"

BorderStyle="Ridge" BorderWidth="2px" Width="300px">

Figure 4: Panel control displayed as balloon tooltip.

 

Next, drag and drop a GridView control on the Web form and add one TemplateField to it. The TemplateField will consist of ImageButton, HyperLink, Label, and PopupControlExtender controls. The ImageButton control displays the folder icon; upon clicking on the folder icon, the balloon with information about the forum is displayed. The HyperLink control simply allows the user to navigate to the threads of that forum; the Label control displays the description of the forum. The PopupControlExtender is an AJAX extender control that displays a popup using client-side script. The popup displayed by the PopupControlExtender can be static or dynamic. The complete markup of this TemplateField is shown in Figure 5.

 

ImageUrl="~/Images/Folder.gif"

OnClientClick="return false;" />

Font-Size="14px" NavigateUrl='<%# Eval

("CategoryId","~/ShowThreads.aspx?categoryid={0}") %>'

Text='<%# Eval("Name") %>'>

Text='<%# Eval("Description") %>'>

ID="PopupControlExtender1" runat="server"

DynamicServicePath="WebService.asmx"

DynamicContextKey='<%# Eval("CategoryId") %>'

DynamicControlID="Label2"

DynamicServiceMethod="GetForumDetails"

PopupControlID="Panel1"

TargetControlID="ImageButton1"

Position="Right">

Figure 5: TemplateField containing PopupControlExtender.

 

Notice the markup shown in bold letters. The ImageUrl property of the ImageButton points to the folder icon from the Images folder of the Web site and its OnClientClick property is set to return false. This way, even after clicking the ImageButton, it won t cause any postback. The ImageButton is used for the purpose of displaying the balloon; it doesn t perform any server-side operation, so it is unnecessary to cause any postback. The NavigateUrl property of the HyperLink control is bound to the CategoryId column using the Eval method. Notice the second parameter of the Eval method. It specifies the format of the URL that will be displayed in the browser. At run time, in place of {0}, the actual value of CategoryId column will be replaced. The Label control is simply bound to the Description columns. Now comes the important part. The PopupControlExtender control sets many important properties. A list of these properties is shown in Figure 6.

 

Property

Description

DynamicServicePath

Specifies the path of the Web service file we wish to consume.

DynamicServiceMethod

Indicates name of Web method that will be called by the PopupControlExtender control.

DynamicContextKey

Specifies the value that will be supplied as a parameter to the Web method as indicated by the DynamicServiceMethod property.

TargetControlID

Indicates the ID of a server control, clicking on which will display the popup.

PopupControlID

Indicates the ID of a server control that will be displayed as a popup.

DynamicControlID

Indicates the ID of a server control that will be assigned the return value of the Web method as specified by the DynamicServiceMethod property.

Position

Specifies the position of the popup with respect to the target control.

Figure 6: Properties of the PopupControlExtender control.

 

This completes the default Web form. Running it in the browser should display a list of forums, as shown in Figure 1.

 

Displaying All Threads from a Forum

Once a user selects a particular forum, all the threads belonging to that forum are to be displayed. This is done on another Web form named ShowThreads.aspx. The overall layout of ShowThreads.aspx is shown in Figure 7.

 


Figure 7: Displaying all threads of a forum in ShowThreads.aspx.

 

The ShowThreads.aspx Web form shows the title of the forum selected by the user on the default page. All the threads belonging to that forum are displayed in a GridView, along with the owner s name and post date. The Start a new thread hyperlink takes the user to another page, wherein a new post can be made.

 

To design the ShowThreads.aspx Web form, drag and drop a Literal control on it. This Literal control will display the title of the forum whose threads are being displayed. Also, drag and drop a SQL data source control and configure it to select the record for the forum whose ID is passed in the query string (see Figure 8).

 


Figure 8: Picking the forum category ID from the query string.

 

This will add a QueryStringParameter to the SQL data source control. The complete markup of the SQL data source control is shown in Figure 9.

 

ConnectionString="<%$

ConnectionStrings:ForumDbConnectionString %>"

SelectCommand="SELECT [Name] FROM [ForumCategories] WHERE

([CategoryId] = @CategoryId)">

QueryStringField="categoryid" Type="Int32" />

Figure 9: Markup of the SQL data source with a QueryStringParameter.

 

Now drag and drop another SQL data source control and configure it to fetch from the ForumThreads table all the records with the following criteria:

  • The CategoryId column value matches with the one passed via query string
  • The IsApproved column value is True
  • The ParentId column value is 0

 

We want to display only the posts approved by the moderators; hence, we check for the IsApproved column value to be True. Similarly, we want to list only the original posts and not replies in the ShowThreads.aspx page, so we fetch only the records whose ParentId is 0. The complete markup of this SQL data source is shown in Figure 10.

 

ConnectionString="<%$ ConnectionStrings:ForumDbConnectionString %>"

SelectCommand="SELECT [Id], Build Community with a Discussion Forum: Part II, [PostedOn], [PostedBy],

[CategoryId] FROM [ForumThreads]

WHERE (([CategoryId] = @CategoryId) AND

([IsApproved] = @IsApproved) AND

([ParentId] = @ParentId)) ORDER BY [PostedOn] DESC">

QueryStringField="categoryid" Type="Int32" />

Figure 10: Selecting all the approved posts.

 

Now drag and drop a GridView control and set its DataSourceID property to the ID of the SQL data source we just configured. Then add one HyperLinkField and two BoundFields to it (see Figure 11).

 


Figure 11: Columns of the GridView that displays threads.

 

Set the properties of the HyperLinkField and BoundFields as shown in Figure 12.

 

DataNavigateUrlFields="CategoryId,Id"

DataNavigateUrlFormatString="~/

ShowThread.aspx?categoryid={0}&threadid={1}"

DataTextField="Title" HeaderText="Title">

HeaderText="Posted By">

DataFormatString="{0:d}" HeaderText="Posted On">

Figure 12: Configuring columns of the GridView.

 

Notice the properties shown in bold letters. The DataNavigateUrlFields property of the HyperLinkField specifies a comma-separated list of column names whose value will be used to generate the resultant URL. The DataNavigateUrlFormatString property specifies the format of the resultant URL. At run time, {0} will be replaced by the value from the CategoryId column, and {1} will be replaced with the value from the Id column. The DataField property of the BoundField columns indicates the name of the table column bound to the BoundField. The PostedOn field value is formatted as a short date using the {0:d} formatting expression.

 

Now add a HyperLink above the GridView and set its Text property to Start a new thread . The NavigateUrl property of this HyperLink and the Text property of the Literal control we added earlier are set via code, as shown in Figure 13.

 

protected void Page_Load(object sender, EventArgs e)

{

DataView dv =(DataView)SqlDataSource2.

Select(DataSourceSelectArguments.Empty);

Literal1.Text = "

" + dv[0]["Name"].ToString() + "

";

HyperLink1.NavigateUrl = "~/showthread.aspx?categoryid=" +

Request.QueryString["categoryid"] + "&threadid=-1";

}

Figure 13: Setting the forum heading.

 

We call the Select method of the SQL data source manually. The Select method returns the data in the form of a DataView. The Text property of the Literal control is then set by picking up the forum name from the DataView. The NavigateUrl property of the HyperLink is set to ShowThread.aspx, and the forum category ID and thread ID are passed as a query string. The thread ID -1 indicates this is a new post and not a reply to any existing post.

 

Displaying a Particular Thread

When a user selects a particular thread from a forum, all the messages belonging to that thread (original post, as well as replies) should be displayed. This is done in another Web form named ShowThread.aspx. Figure 14 shows how this Web form should look.

 


Figure 14: Layout of ShowThread.aspx.

 

ShowThread.aspx has standard forum features such as posting a message, replying to an existing post, and replying with quotes. To begin developing ShowThread.aspx, drag and drop a Literal control and a SQL data source control on it. Configure the SQL data source to fetch the forum name from the database exactly as we did earlier. Drag and drop another SQL data source and configure it to fetch all the records from the ForumThreads table whose:

  • IsApproved column value is True
  • ParentId column value is equal to the thread ID passed in the query string OR Id column value is equal to the thread ID passed in the query string

 

This way, we ll get the original post and all its replies. To display them in the same order as post date, sort them on the PostedOn column. Figure 15 shows the complete markup of the SQL data source control. Next, drag and drop a DataList control on the form and design its ItemTemplate as shown in Figure 16. Then data bind the Label controls to the Title, Description, PostedBy, and PostedOn columns, respectively (see Figure 17).

 

ConnectionString="<%$ ConnectionStrings:ForumDbConnectionString %>"

SelectCommand="SELECT * FROM [ForumThreads] WHERE ([IsApproved] =

@IsApproved) AND ([ParentId] = @ParentId OR [email protected]) ORDER BY [PostedOn]">

Figure 15: Fetching all records belonging to a single thread.

 


Figure 16: ItemTemplate of the DataList.

 


Figure 17: Binding Label controls to display post details.

 

Also, set the CommandName property of the Reply button to Reply. This way we ll be able to identify in the code that the Reply button has been hit. Below the DataList, drag and drop an UpdatePanel control and design its ContentTemplate, as shown in Figure 18. Then drag and drop a SQL data source control inside the UpdatePanel and configure it to INSERT a record in the ForumThreads table (see Figure 19).

 


Figure 18: Posting a new message.

 


Figure 19: Inserting a new record in the ForumThreads table.

 

Now it s time to handle some events to make ShowThread.aspx functional. First, write the Page_Load event handler as shown in Figure 20. This code should be familiar to you, as it matches what we wrote in ShowThreads.aspx. When a user hits the Reply button, we need to populate various textboxes. This is done in the ItemCommand event handler of the DataList (see Figure 21).

 

protected void Page_Load(object sender, EventArgs e)

{

DataView dv = (DataView)SqlDataSource1.Select

(DataSourceSelectArguments.Empty);

Literal1.Text = "

" + dv[0]["Name"].ToString() + "

";

}

Figure 20: Displaying the title of the forum.

 

protected void DataList1_ItemCommand

(object source, DataListCommandEventArgs e)

{

 if (e.CommandName == "Reply")

 {

 CheckBox cb = (CheckBox)e.Item.FindControl("CheckBox1");

 Label title=(Label)e.Item.FindControl("Label1");

 Label desc=(Label)e.Item.FindControl("Label2");

 Label by = (Label)e.Item.FindControl("Label3");

 Label on = (Label)e.Item.FindControl("Label4");

 if (cb.Checked)

 {

   TextBox2.Text = "[quote]" + desc.Text + "\r\n" +

   by.Text + " " + on.Text + @"[\quote]";

 }

 if (!title.Text.StartsWith("Re:"))

 {

   TextBox1.Text = "Re:" + title.Text;

 }

 }

}

Figure 21: The ItemCommand event handler of the DataList.

 

In the ItemCommand event handler we first check if the CommandName property is Reply. We then decide if the Reply with quotes checkbox is selected. If so, we place the original post in [quote] and [\quote] tags. These tags simply act as markers so we know which part belongs to the original post. We also set the default title of the reply by prefixing the original title with Re: . The actual job of inserting a reply is done in the Click event handler of the Submit button. Figure 22 shows this event handler.

 

protected void Button2_Click(object sender, EventArgs e)

{

 int categoryid=int.Parse(Request.QueryString["categoryid"]);

 int threadid=int.Parse(Request.QueryString["threadid"]);

 if(threadid==-1)

 {

   SqlDataSource3.InsertParameters["ParentId"].DefaultValue = "0";

 }

 else

 {

   SqlDataSource3.InsertParameters["ParentId"].DefaultValue =

   threadid.ToString();

 }

 SqlDataSource3.InsertParameters["CategoryId"].DefaultValue =

 categoryid.ToString();

 SqlDataSource3.InsertParameters["Title"].DefaultValue = TextBox1.Text;

 SqlDataSource3.InsertParameters["Description"].DefaultValue =

 TextBox2.Text;

 SqlDataSource3.InsertParameters["PostedBy"].DefaultValue =

 Membership.GetUser().UserName;

 SqlDataSource3.InsertParameters["PostedOn"].DefaultValue =

 DateTime.Now.ToString();

 SqlDataSource3.InsertParameters["IsApproved"].DefaultValue = "False";

 SqlDataSource3.Insert();

 Label7.Text="Your message has been submitted for approval";

 Panel1.Visible=false;

}

Figure 22: Adding a post.

 

The code first decides if the post is the start of a new thread or the reply to an existing thread. This is done by checking the threaded query string parameter. If this parameter is -1 we know that the post is the start of a new thread. Accordingly, the ParentId parameter of the SQL data source is set to 0. Other parameters of the SQL data source are also set. Finally, the Insert method of the SQL data source control is called to add the record in the ForumThreads table. A success message is displayed and the panel is hidden from the user.

 

While rendering all the posts belonging to a thread we need to display the text within [quote] and [\quote] marks with some different coloring so users can distinguish it from the rest of the message. This is done in the ItemDataBound event of the DataList.

 

protected void DataList1_ItemDataBound

(object sender, DataListItemEventArgs e)

{

 Label desc = (Label)e.Item.FindControl("Label2");

 string newDesc = desc.Text.Replace

  ("[quote]", "

");

 newDesc = newDesc.Replace(@"[\quote]", "

");

 newDesc=newDesc.Replace("\r\n", "
");

 desc.Text = newDesc;

}

Figure 23: Displaying quotes in a different color.

 

We replace the [quote] and [\quote] tags with

and
tags, respectively. The carriage return and line feed characters are replaced with a
tag so an HTML line break is rendered.

 

Developing Administrative Pages

Now that we have the main forum pages ready, it s time for developing administrative pages. The first Web form we ll develop allows the moderator to manage forum categories (ManageCategories.aspx); see Figure 24.

 


Figure 24: Managing forum categories.

 

The page consists of a DropDownList displaying all the existing forum categories. Upon selecting a particular category, its details are displayed in a DetailsView. In addition to normal Edit, New, and Delete buttons, the DetailsView has one more button titled More Info. Clicking this button displays a balloon with more information about the forum category, such as description, total number of posts and threads, and the latest post.

 

Add a ScriptManagerProxy control to the Web form and add the WebService.asmx service to its Services collection. Then drag and drop an UpdatePanel in the form and design its ContentTemplate to look like Figure 24. In this case, we need two SQL data source controls: one that supplies data to the DropDownList control and the other that supplies data to the DetailsView. The first SQL data source control selects all the records from the ForumCategories table, whereas the other selects just one record from the ForumCategories table that matches the selected forum category ID. Make sure to generate INSERT, UPDATE, and DELETE statements for the second SQL data source control. Their markups are shown in Figure 25.

 

SelectCommand="SELECT * FROM [ForumCategories] ORDER BY [Name]">

DeleteCommand="DELETE FROM [ForumCategories] WHERE

[CategoryId] = @CategoryId"

InsertCommand="INSERT INTO [ForumCategories] ([Name],

[Description]) VALUES (@Name, @Description)"

UpdateCommand="UPDATE [ForumCategories] SET [Name] = @Name,

[Description] = @Description WHERE [CategoryId] = @CategoryId"

SelectCommand="SELECT * FROM [ForumCategories] WHERE ([CategoryId] =

@CategoryId)">

Type="Int32" Name="CategoryId" ControlID="DropDownList1">

Figure 25: SQL data source that supplies data to the DropDownList.

 

The DetailsView has two BoundFields and two TemplateFields. The CategoryID and Name are BoundFields, whereas Description is a TemplateField. The second TemplateField contains all the action buttons, such as Edit and Delete. More important, however, is the More Info button. Here we use the PopupControlExtender control again. The relevant markup is shown in Figure 26.

 

runat="server" Position="Right"

TargetControlID="Button4"

PopupControlID="Panel1"

DynamicServiceMethod="GetForumDetailsForModerator"

DynamicControlID="Label2"

DynamicContextKey='<%# Eval("CategoryId") %>'

DynamicServicePath="../WebService.asmx">

Figure 26: PopupControl extender for the More Info button.

 

The markup in Figure 26 should be familiar to you, as we used it while developing the default Web form. The PopupControlExtender simply calls our GetForumDetailsForModerator Web method. The DynamicContextKey, DynamicServicePath, DynamicServiceMethod, DynamicControlID, and PopupControlID properties are set as before.

The ManageCategories.aspx should be accessible only to the moderators (or administrators) of the application, so we need to have role-based security in place. The Page_Load event handler of ManageCategories.aspx does that checking (see Figure 27).

 

protected void Page_Load(object sender, EventArgs e)

{

 if (!Roles.IsUserInRole("Moderator"))

 {

   throw new Exception("You are not authorized

   to view this page!");

 }

}

Figure 27: Role-based security.

 

The code checks if the currently logged-in user belongs to the Moderator role using the IsUserInRole method of the Roles object. If not, an exception is thrown. The second administrative page (ManageThreads.aspx) allows the moderator to approve or reject forum posts. This Web form is shown in Figure 28.

 


Figure 28: Moderating forum threads.

 

To begin developing this form, drag and drop a ScriptManagerProxy control on it and add WebService.asmx to its Services collection. Then add an UpdatePanel on the form. Drag and drop a Timer control inside the UpdatePanel and set its Interval property to 5000 (milliseconds). This will cause the UpdatePanel to refresh automatically every five seconds. This way the moderators can see new posts as they are submitted by the users.

 

Next, add two SQL data source controls on the form. Configure the first one to select all the records from the ForumThreads table that are awaiting approval (IsApproved = False) and the other to select one particular record based on its ID. Their markups are shown in Figure 29.

 

...

SelectCommand="SELECT [ParentId], [CategoryId] FROM

[ForumThreads] WHERE ([Id] = @Id)">

...

SelectCommand="SELECT * FROM [ForumThreads] WHERE

([IsApproved] = @IsApproved) ORDER BY [PostedOn]">

Name="IsApproved">

...

Figure 29: SQL data sources on ManageThreads.aspx.

 

Now place a GridView control inside the UpdatePanel and design its ItemTemplate as shown in Figure 30.

 


Figure 30: ItemTemplate of GridView.

 

The Text property of the HyperLink is bound to the Title column. Similarly, the Text properties of the Label controls are bound to the Description, PostedBy, and PostedOn columns, respectively. The Approve and Reject buttons call some JavaScript functions (discussed later), so as to approve or reject a post, respectively. The JavaScript function calls are wired in the RowDataBound event handler of the GridView (see Figure 31).

 

protected void GridView1_RowDataBound

(object sender, GridViewRowEventArgs e)

{

 if (e.Row.RowType == DataControlRowType.DataRow)

 {

    int id = Convert.ToInt32(GridView1.DataKeys[e.Row.RowIndex].Value);

   SqlDataSource2.SelectParameters["Id"].DefaultValue = id.ToString();

   DataView dv = (DataView)SqlDataSource2.

   Select(DataSourceSelectArguments.Empty);

   int parentid = Convert.ToInt32(dv[0]["ParentId"]);

   int categoryid = Convert.ToInt32(dv[0]["CategoryId"]);

   Button b1 = (Button)e.Row.FindControl("Button1");

   Button b2 = (Button)e.Row.FindControl("Button2");

   b1.OnClientClick = "return ApprovePost(" + id + ")";

    b2.OnClientClick = "return RejectPost(" + id + ")";

   HyperLink lnk = (HyperLink)e.Row.FindControl("HyperLink1");

   if (parentid == 0)

   {

     lnk.NavigateUrl = "~/ShowThread.aspx?categoryid=" +

     categoryid.ToString() + "&threadid=" + id.ToString();

   }

   else

   {

     lnk.NavigateUrl = "~/ShowThread.aspx?categoryid=" +

     categoryid.ToString() + "&threadid=" + parentid.ToString();

   }

 }

}

Figure 31: RowDataBound event handler of GridView.

 

For each DataRow of the GridView we fetch a single row matching the Id of the post. The Id, ParentId, and CategoryId of that post are stored in local variables. On the client-side click event of the Approve and Reject buttons, we call two JavaScript functions, namely ApprovePost and RejectPost. These functions accept the post ID as the parameter. The NavigateUrl property of the HyperLink is then pointed to ShowThread.aspx, along with categoryid and threaded query string parameters. The ApprovePost and RejectPost JavaScript functions are shown in Figure 32.

 

Figure 32: Calling Web service via client script.

 

The ApprovePost function calls the ApprovePost Web method. Notice that in addition to passing the postId, we also pass three function references. These functions OnApproveSuccess, OnError, and OnTimeout are called after successful completion, error, and timeout of the Web method, respectively. These functions simply display appropriate alert boxes to the end user. Along the same lines, the RejectPost JavaScript function calls the RejectPost Web method. That s it! Our forum application is now ready.

 

Conclusion

To conclude this two-part series we developed main forum pages and administrative pages. The Default.aspx shows a list of available forums, ShowThreads.aspx shows a list of all the messages from a selected forum, and ShowThread.aspx shows all the messages belonging to a single thread. ASP.NET AJAX provides easy ways to improve a user s experience. The balloon help, post-back reduction by using UpdatePanel, and consuming Web service from client script are some of the examples of applying ASP.NET AJAX features. The AJAX Control Toolkit further adds to the spice by providing many cool controls PopControlExtender for example ready to use with minimal coding. Together, these features help you improve user experience and performance.

 

The source code accompanying this article is available for download.

 

Bipin Joshi is the proprietor of BinaryIntellect Consulting (http://www.binaryintellect.com) where he conducts premier training programs on a variety of .NET technologies. He wears many hats, including software consultant, mentor, prolific author, Webmaster, Microsoft MVP, and member of ASPInsiders. Having adopted Yoga as a way of life, Bipin also teaches Kriya Yoga. He can be reached via his blog at http://www.bipinjoshi.com.

 

 

 

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish