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!
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>
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)
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 )
One approach I remember seeing somewhere was rather than using a Comment ID and a Parent ID , comments had a Comment ID and a "sort key" which was a concatenation of all the Comment IDs of their ancestors.
E.g. If comment 1 had two replies, comments 2 and 3, the sort keys would be:
1 : 0001
2 : 0001.0002
3 : 0001.0003
Then if someone replied to comment 2, it would be..
4 : 0001.0002.0004
So if you select all comments and sort by this sort key, they'll fall out in the right order.
Then, to do the indenting, you simply look at the length of the sort key to see how many levels deep the comment is, and indent an appropriate amount.
Adding comments is easy: the sort key of the new comment is simply it's parent's sort key, with its own ID added on to the end.