Hi, I am working on creating a blog in asp.net 4.0 and sql server 2008 and would like to learn how to create a threaded comments system. By threaded I mean each comment would have a reply link and the comments are indented under the comment that it is a reply to. So, you can either respond to the article itself or reply to any of the comments.
This is very common on forums and blogs but I cannot find any articles that would explain and show code example on how this is done. The following is what I have created but it only works one level of depth. I'd like to make it recursive so there is no limit to the level of depth:. How can I do this? Any advice, articles with code samples would be awesome! Thank you!
My Comments db Table:
commentId
parentId
postId
date
author
authorEmail
authorURL
authorIP
content
IsApproved
ASP.NET Markup:
<asp:ListView ID="ListView1" runat="server" onitemdatabound="ListView1_ItemDataBound">
<ItemTemplate>
<div class="commentwrap">
<div class="commentsTitleArea">
<span class="commentCounter"><%# Convert.ToInt32(Container.DisplayIndex) + 1%>. </span> <img src="../images/decoy-icon-16px.png" alt="Comment by..." title="Comment by..." class="blogCommentIcon" /><a href='<%# Eval("AuthorUrl")%>' target="_blank" rel="nofollow"><%# " " + Eval("Author")%></a> <%# Eval("Date")%></div>
<div class="commentText">
<%# Eval("Content") %>
<div><span class="btnCommentReply"><a href='<%# "article.aspx?article=" + Request.QueryString["article"] + "&cid=" + Eval("commentId") + "#comment" %>'>REPLY</a></span></div>
</div>
<asp:ListView ID="ListView2" runat="server">
<ItemTemplate>
<div class="commentwrap commentNest">
<div class="commentsTitleArea">
<span class="commentCounter"><%# Convert.ToInt32(Container.DisplayIndex) + 1%>. </span> <img src="../images/decoy-icon-16px.png" alt="Comment by..." title="Comment by..." class="blogCommentIcon" /><a href='<%# Eval("AuthorUrl")%>' target="_blank" rel="nofollow"><%# " " + Eval("Author")%></a> <%# Eval("Date")%></div>
<div class="commentText">
<%# Eval("Content") %>
</div>
</div>
</ItemTemplate>
<EmptyDataTemplate>
</EmptyDataTemplate>
<LayoutTemplate>
<div id="itemPlaceholderContainer" runat="server">
<span id="itemPlaceholder" runat="server" />
</div>
</LayoutTemplate>
</asp:ListView>
</div>
</ItemTemplate>
<EmptyDataTemplate>
</EmptyDataTemplate>
<LayoutTemplate>
<div id="itemPlaceholderContainer" runat="server">
<span id="itemPlaceholder" runat="server" />
</div>
<div class="dataPagerWrap">
<asp:DataPager ID="ListViewpager" runat="server" PagedControlID="ListView1" PageSize="30" QueryStringField="page">
<Fields>
<asp:NextPreviousPagerField ShowFirstPageButton="True" ShowNextPageButton="False" ShowPreviousPageButton="False" FirstPageText="«" ButtonCssClass="dataPagerBackForward" />
<asp:NumericPagerField ButtonCount="8" CurrentPageLabelCssClass="dataPagerCurrent" NumericButtonCssClass="dataPager" PreviousPageText="..." NextPageText="..." NextPreviousButtonCssClass="dataPagerBackForward" />
<asp:NextPreviousPagerField ShowLastPageButton="True" ShowNextPageButton="False" ShowPreviousPageButton="False" LastPageText="»" ButtonCssClass="dataPagerBackForward" />
</Fields>
</asp:DataPager>
</div>
<div class="padding"></div>
</LayoutTemplate>
C#:
#region Page_Load - GetComments()
protected void Page_Load(object sender, EventArgs e)
{
GetComments();
}
#endregion
#region get comments method
protected void GetComments()
{
// get required elements
int article = int.Parse(Request.QueryString["article"]);
// connect to database
SqlDataAdapter adapter = new SqlDataAdapter();
SqlConnection con = new SqlConnection(ConfigurationManager.ConnectionStrings["dbMyCMSConnectionString"].ConnectionString);
SqlCommand cmd = new SqlCommand("sp_blog_GetComments", con);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add("@article", SqlDbType.Int).Value = article;
adapter.SelectCommand = cmd;
// add tables to dataset
DataSet ds = new DataSet();
adapter.Fill(ds);
try
{
ds.Relations.Add(new DataRelation("nestThem", ds.Tables[0].Columns["commentId"], ds.Tables[1].Columns["ParentId"]));
}
catch
{
//
}
// ListView1 datasource
ListView1.DataSource = ds;
ListView1.DataBind();
}
#endregion
#region Databind repeaters
protected void ListView1_ItemDataBound(object sender, ListViewItemEventArgs e)
{
DataRowView dv = e.Item.DataItem as DataRowView;
if (dv != null)
{
ListView ListView2 = e.Item.FindControl("ListView2") as ListView;
if (ListView2 != null)
{
ListView2.DataSource = dv.CreateChildView("nestThem");
ListView2.DataBind();
}
}
}
#endregion
Stored Procedure:
ALTER PROCEDURE [dbo].[sp_blog_GetComments]
( @article int ) AS SET NOCOUNT ON;
SELECT post_Comments.Author, post_Comments.AuthorEmail, post_Comments.AuthorUrl, post_Comments.Content, post_Comments.Date, post_Comments.commentId FROM post_Comments INNER JOIN posts ON post_Comments.postId = posts.postId WHERE(post_Comments.postId = @article) AND (post_Comments.IsApproved = 1) AND (post_Comments.ParentId IS NULL) AND (posts.IsPublished = 1) AND (posts.PublishOnDate <= GETDATE())
SELECT Author, AuthorEmail, AuthorUrl, Content, Date, ParentId FROM post_Comments WHERE (postId = @article) AND (IsApproved = 1)