views:

100

answers:

1

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>&nbsp;&nbsp;<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>&nbsp;&nbsp;<%# Eval("Date")%></div>
        <div class="commentText">
            <%# Eval("Content") %>
            <div><span class="btnCommentReply"><a href='<%# "article.aspx?article=" + Request.QueryString["article"] + "&amp;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>&nbsp;&nbsp;<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>&nbsp;&nbsp;<%# 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)

A: 

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.

Carson63000
@Carson63000, Thanks for the reply! What data type would I use for this field?
Scott W.
Just a varchar.. make it long enough to support whatever limit you want to place on number of comments and nesting depth. e.g. if you limit to 9999 comments per post, and replies 5 levels deep, the longest sort key would be like "9999.9999.9999.9999.9999" so you'd need varchar(24)
Carson63000
Carson63000, Thank you for the suggestion. It was a little bit tricky to figure out all the details but I managed to get it done. The hardest part was to get the number of dots to assign the css for the indent. The stored procedures were rather simple. For comments the sorting value is set equal to its own comment id value and for comment replies the value is set equal to the parent sorting id concatenated with it's own comment id separated by a dot. If a sorting id has no dots, it is a top level comment. One dot equals one level depth, two equals two level depth etc. Thank you!
Scott W.