Build Community with a Discussion Forum: Part II

Develop Main Forum Pages and Administrative Pages

Bipin Joshi

October 30, 2009

18 Min Read
ITPro Today logo in a gray background | ITPro Today

CodeTalk

LANGUAGES: C#

ASP.NETVERSIONS: 3.5

 

Build Community with a Discussion Forum: Part II

Develop Main Forum Pages and Administrative Pages

 

By Bipin Joshi

 

In PartI of this article we discussed the overall functional requirements and thelogic of a discussion forum application. We also created the necessary databasetables, as well as AJAX-enabled log-in and registration pages. In addition, wedeveloped a Web service with helper Web methods to be consumed from client-sidescript. Now it s time to develop the Web forms that display forums and threads.Any forum application needs some facility for administering and moderatingforum 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 theWeb site should display a list of available forums (see Figure 1). Notice howthe balloon with information about a forum is displayed. The balloon displaysthe description of the forum along with total post and thread information. Theballoon is displayed using AJAX Web service calls.

 


Figure 1: Displaying a list ofavailable forums.

 

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

 

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

 


Figure 2: Adding a reference toWebService.asmx.

 


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

 

BackColor="FloralWhite"BorderColor="Gainsboro" BorderStyle="Ridge" BorderWidth="2px"Width="300px">

Figure 4: Panel controldisplayed as balloon tooltip.

 

Next, drag and drop a GridView control on the Web form andadd one TemplateField to it. The TemplateField will consist of ImageButton,HyperLink, Label, and PopupControlExtender controls. The ImageButton control displaysthe folder icon; upon clicking on the folder icon, the balloon with informationabout the forum is displayed. The HyperLink control simply allows the user tonavigate to the threads of that forum; the Label control displays thedescription of the forum. The PopupControlExtender is an AJAX extender controlthat displays a popup using client-side script. The popup displayed by thePopupControlExtender can be static or dynamic. The complete markup of thisTemplateField 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 ImageUrlproperty of the ImageButton points to the folder icon from the Images folder ofthe Web site and its OnClientClick property is set to return false. This way,even after clicking the ImageButton, it won t cause any postback. TheImageButton is used for the purpose of displaying the balloon; it doesn tperform any server-side operation, so it is unnecessary to cause any postback.The NavigateUrl property of the HyperLink control is bound to the CategoryIdcolumn 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. Atrun time, in place of {0}, the actual value of CategoryId column will bereplaced. The Label control is simply bound to the Description columns. Nowcomes the important part. The PopupControlExtender control sets many importantproperties. 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 browsershould 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 threadsbelonging to that forum are to be displayed. This is done on another Web formnamed ShowThreads.aspx. The overall layout of ShowThreads.aspx is shown inFigure 7.  
Figure 7: Displaying all threads ofa forum in ShowThreads.aspx.   The ShowThreads.aspx Web form shows the title of the forumselected by the user on the default page. All the threads belonging to thatforum 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 newpost can be made.   To design the ShowThreads.aspx Web form, drag and drop aLiteral control on it. This Literal control will display the title of the forumwhose threads are being displayed. Also, drag and drop a SQL data sourcecontrol and configure it to select the record for the forum whose ID is passedin the query string (see Figure 8).  
Figure 8: Picking the forum categoryID from the query string.   This will add a QueryStringParameter to the SQL datasource control. The complete markup of the SQL data source control is shown inFigure 9.   ConnectionString="<%$ ConnectionStrings:ForumDbConnectionString %>" SelectCommand="SELECT [Name] FROM [ForumCategories] WHERE ([CategoryId] = @CategoryId)"> QueryStringField="categoryid" Type="Int32" /> Figure 9: Markupof the SQL data source with a QueryStringParameter.   Now drag and drop another SQL data source control andconfigure it to fetch from the ForumThreads table all the records with the followingcriteria:The CategoryId column value matches with the onepassed via query stringThe IsApproved column value is TrueThe ParentId column value is 0   We want to display only the posts approved by themoderators; hence, we check for the IsApproved column value to be True.Similarly, we want to list only the original posts and not replies in theShowThreads.aspx page, so we fetch only the records whose ParentId is 0. Thecomplete 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 itsDataSourceID property to the ID of the SQL data source we just configured. Thenadd one HyperLinkField and two BoundFields to it (see Figure 11).  
Figure 11: Columns of the GridViewthat displays threads.   Set the properties of the HyperLinkField and BoundFieldsas 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="PostedOn"> Figure 12:Configuring columns of the GridView.   Notice the properties shown in bold letters. The DataNavigateUrlFieldsproperty of the HyperLinkField specifies a comma-separated list of column nameswhose value will be used to generate the resultant URL. TheDataNavigateUrlFormatString 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 propertyof the BoundField columns indicates the name of the table column bound to theBoundField. 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 Textproperty to Start a new thread . The NavigateUrl property of this HyperLinkand 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: Settingthe 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 propertyof the Literal control is then set by picking up the forum name from theDataView. 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. Thethread 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, allthe 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 asposting a message, replying to an existing post, and replying with quotes. Tobegin developing ShowThread.aspx, drag and drop a Literal control and a SQLdata source control on it. Configure the SQL data source to fetch the forumname from the database exactly as we did earlier. Drag and drop another SQLdata source and configure it to fetch all the records from the ForumThreadstable whose: IsApproved column value is TrueParentId column value is equal to the thread IDpassed in the query string OR Id column value is equal to the thread ID passedin 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 PostedOncolumn. 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 asshown 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 Id=@ID) ORDER BY[PostedOn]"> Figure 15:Fetching all records belonging to a single thread.  
Figure 16: ItemTemplate of theDataList.  
Figure 17: Binding Label controls todisplay post details.   Also, set the CommandName property of the Reply button toReply. This way we ll be able to identify in the code that the Reply button hasbeen hit. Below the DataList, drag and drop an UpdatePanel control and designits ContentTemplate, as shown in Figure 18. Then drag and drop a SQL datasource control inside the UpdatePanel and configure it to INSERT a record inthe ForumThreads table (see Figure 19).  
Figure 18: Posting a new message.  
Figure 19: Inserting a new record inthe ForumThreads table.   Now it s time to handle some events to makeShowThread.aspx functional. First, write the Page_Load event handler as shownin Figure 20. This code should be familiar to you, as it matches what we wrotein ShowThreads.aspx. When a user hits the Reply button, we need to populatevarious textboxes. This is done in the ItemCommand event handler of theDataList (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");

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

 Labeldesc=(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" +

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

 }

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

 {

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

 }

 }

}

Figure 21: The ItemCommandevent handler of the DataList.

 

In the ItemCommand event handler we first check if theCommandName property is Reply. We then decide if the Reply with quotes checkboxis 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 originalpost. We also set the default title of the reply by prefixing the originaltitle with Re: . The actual job of inserting a reply is done in the Clickevent handler of the Submit button. Figure 22 shows this event handler.

 

protected void Button2_Click(object sender, EventArgs e)

{

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

 intthreadid=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="Yourmessage has been submitted for approval";

 Panel1.Visible=false;

}

Figure 22: Addinga post.

 

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

 

While rendering all the posts belonging to a thread weneed to display the text within [quote] and [quote] marks with some differentcoloring so users can distinguish it from the rest of the message. This is donein 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", "
");

 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 feedcharacters are replaced with a
tag so an HTML line break isrendered.

 

Developing Administrative Pages

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

 


Figure 24: Managing forum categories.

 

The page consists of a DropDownList displaying all theexisting forum categories. Upon selecting a particular category, its detailsare displayed in a DetailsView. In addition to normal Edit, New, and Deletebuttons, the DetailsView has one more button titled More Info. Clicking thisbutton displays a balloon with more information about the forum category, suchas description, total number of posts and threads, and the latest post.

 

Add a ScriptManagerProxy control to the Web form and add theWebService.asmx service to its Services collection. Then drag and drop anUpdatePanel 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 tothe DropDownList control and the other that supplies data to the DetailsView.The first SQL data source control selects all the records from the ForumCategoriestable, whereas the other selects just one record from the ForumCategories tablethat matches the selected forum category ID. Make sure to generate INSERT,UPDATE, and DELETE statements for the second SQL data source control. Theirmarkups 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: SQLdata source that supplies data to the DropDownList.

 

The DetailsView has two BoundFields and two TemplateFields.The CategoryID and Name are BoundFields, whereas Description is aTemplateField. The second TemplateField contains all the action buttons, suchas Edit and Delete. More important, however, is the More Info button. Here weuse the PopupControlExtender control again. The relevant markup is shown inFigure 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 weused it while developing the default Web form. The PopupControlExtender simplycalls our GetForumDetailsForModerator Web method. The DynamicContextKey,DynamicServicePath, DynamicServiceMethod, DynamicControlID, and PopupControlIDproperties are set as before.

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

 

protected void Page_Load(object sender, EventArgs e)

{

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

 {

   throw newException("You are not authorized

   to view thispage!");

 }

}

Figure 27: Role-basedsecurity.

 

The code checks if the currently logged-in user belongs tothe 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 shownin Figure 28.

 


Figure 28: Moderating forum threads.

 

To begin developing this form, drag and drop aScriptManagerProxy control on it and add WebService.asmx to its Servicescollection. Then add an UpdatePanel on the form. Drag and drop a Timer controlinside 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 tablethat are awaiting approval (IsApproved = False) and the other to select oneparticular 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: SQLdata sources on ManageThreads.aspx.

 

Now place a GridView control inside the UpdatePanel anddesign 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 theDescription, PostedBy, and PostedOn columns, respectively. The Approve andReject buttons call some JavaScript functions (discussed later), so as toapprove or reject a post, respectively. The JavaScript function calls are wiredin 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 = "returnRejectPost(" + 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 rowmatching the Id of the post. The Id, ParentId, and CategoryId of that post arestored in local variables. On the client-side click event of the Approve andReject buttons, we call two JavaScript functions, namely ApprovePost andRejectPost. These functions accept the post ID as the parameter. TheNavigateUrl property of the HyperLink is then pointed to ShowThread.aspx, alongwith categoryid and threaded query string parameters. The ApprovePost andRejectPost JavaScript functions are shown in Figure 32.

 

function ApprovePost(postId) {   WebService.ApprovePost(postId,OnApproveSuccess,    OnError,OnTimeout); }function RejectPost(postId) {   WebService.RejectPost(postId,OnRejectSuccess,    OnError,OnTimeout); }function OnApproveSuccess(result) {   alert('Post approvedsuccessfully!'); }function OnRejectSuccess(result) {   alert('Post deletedsuccessfully!'); }function OnError(err) {   alert(err.get_message());}function OnTimeout(err) {   alert('The operationtimeout!'); }

Figure 32: CallingWeb service via client script.

 

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

 

Conclusion

To conclude this two-part series we developed main forumpages and administrative pages. The Default.aspx shows a list of availableforums, 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 balloonhelp, post-back reduction by using UpdatePanel, and consuming Web service fromclient script are some of the examples of applying ASP.NET AJAX features. TheAJAX 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 codeaccompanying this article is available for download.

 

Bipin Joshi is theproprietor 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.

 

 

 

Sign up for the ITPro Today newsletter
Stay on top of the IT universe with commentary, news analysis, how-to's, and tips delivered to your inbox daily.

You May Also Like