Comments
12 Jun 2009 @ 08:28AM

Updated: 27 Jan 2010 @ 08:51AM
We've come to the final section of our blog, the ability to leave comments. Our rules were to allow anyone to post a response to a blog via a comment, and in fact to be able to respond to any other comment as well. This requires the ability to nest comments beneath one another. So let's get started with the easy part: a link to view and post comments.
     content += @"</div>
          <div class='blogSubTitle'>
               <div class='author'>By "
+ dr["displayName"] + dr["email"] + @"</div>
               <div class='date'>"
+ Convert.ToDateTime(dr["displayDate"]).ToString("hh:mmtt dd MMM yyyy") + @"</div>
          </div>
          <div class='text'>"
+ Convert.ToString(dr["blogText"]).Replace("n", "<br>") + @"</div>
          <div class='comments'><a href='default.aspx?option=comments&blogID="
+ dr["blogID"] + "'>Comments (" + dr["Comments"] + @") </a></div>
     </div>"
;

We're just setting up a link so we can view comments on a blog.
Comments (0)
Let's code the Page_Load() portion next.
          case "comments":
               showblogs = false;
               try
               {
                    blogID = Convert.ToInt16(getVariable("blogID", var.GET));
               }
               catch { }
               content += comments(blogID);
               break;

This should look quite familiar by now. The plan is to show the blog and then all the comments below it. I could just put all that in the comments() method, copying the appropriate portion from the showBlogs() method. However, one of the biggest rules to remember is never to code the same thing twice. Always reuse existing code, otherwise if you have to change something, you have to change it in multiple places. In reality, if you have the same code in two places and you have to go back to make a change in 6 months, you'll forget about the second place and may run into some major problems as a result. So instead of duplicating code, I made some changes to the showBlogs() method to accomodate displaying a specific blog.
private string showBlogs(int blogID)
{
     string content = null;
     string select = "SELECT top 30 blogID, blogTitle, blogText, displayName, displayDate, " +
          "CASE " +
               "WHEN showEmail = 1 THEN ' (<a href="mailto: ' + email + '">' + email + '</a>)' " +
               "ELSE '' " +
          "END AS 'email', " +
           "(SELECT COUNT(*) FROM comments WHERE blogID=a.blogID and visible=1) as Comments, " +
           "b.userID " +
     "FROM blogs a " +
          "JOIN users b on " +
               "a.userID=b.userID " +
     "WHERE visible=1 ";
     select += (blogID == 0)? "ORDER BY displayDate Desc" : " AND blogID=" + blogID;

This is the first few lines of the showBlogs() method. I've added the ability to pass an integer called blogID into the method. Then when I'm building the select statement, I check that variable. If it's 0, I just do what I usually was doing. If it's greater than 0, I pass in the blogID in the WHERE clause. I don't bother to ORDER the result if I'm passing in a blogID... I'll only ever get 1 result that way.
Comments (0)
if (dr.HasRows)
{
     while (dr.Read())
     {
          ...
          if(blogID == 0){
               content += "<div class='comments'><a href='default.aspx?option=comments&blogID=" + dr["blogID"] + "'>Comments (" + dr["Comments"] + @") </a></div>";
          }
          else
          {
               content += "<div class='comments'><a href='default.aspx?option=postComment&blogID=" + dr["blogID"] + "'>Post Comment</a></div>";
          }
          
          content += "</div>";
     }
}
else
{
     content = "<div class='error'>";
     content += (blogID > 0) ? "Invalid ID passed" : "There are no blogs currently available";
     content += "</div>";
}

Here is the latter half of the method. I've trimmed out some of the dr.Read() loop for brevity's sake. I check if the blogID is 0 and, if it is, I display the number of comments. If it's anything else, I'll be showing the comments below so this would be pointless. Instead I show the link to post a comment. I also do a HasRows check on the SqlDataReader object. If there aren't any rows, I display an error depending on whether or not a blogID was passed in. If you got lost in here at all, please grab the code files later on and check out the complete code.
Comments (0)
Before I go any further, please note that I made a mistake in the query() method. I failed to add a CloseConnection CommandBeahvior to the ExecuteReader() method. This means that the SQL connections stay open until they time out on their own. This is pretty slow and any rapid queries could run SQL server out of available connections, basically killing the website. I may have mentioned that coding is 10% of writing code and 90% debugging it? Here's the corrected line.
return cm.ExecuteReader(CommandBehavior.CloseConnection);
Comments (0)
With that rather serious bug addressed, let's move on to the hard part... displaying nested comments. I'll post out the code and then explain what we've done.
public struct comment
{
     public int commentID;
     public int userID;
     public int blogID;
     public string displayName;
     public int parentComment;
     public string name;
     public string commentText;
     public DateTime commentDate;
     public string email;
}

private string comments(int blogID)
{
     string content = showBlogs(blogID);
     string select = @"SELECT commentID, displayName, parentComment, name, commentText, commentDate,
                     CASE "
+
               "WHEN showEmail = 1 THEN ' (<a href=\"mailto: ' + email + '\">' + email + '</a>)' " +
               "ELSE '' " +
          @"END AS 'email', a.userID
      FROM comments a
          LEFT JOIN users b
               on a.userID = b.userID
      WHERE blogID="
+ blogID + @"
      ORDER BY commentDate ASC"
;
     SqlDataReader dr = query(select);
     ArrayList comments = new ArrayList();
     while (dr.Read())
     {
          comment current = new comment();
          current.commentID = Convert.ToInt16(dr["commentID"]);
          current.blogID = blogID;
          current.displayName = Convert.ToString(dr["displayName"]);
          current.parentComment = Convert.ToInt16(dr["parentComment"]);
          current.name = Convert.ToString(dr["name"]);
          current.commentText = Convert.ToString(dr["commentText"]);
          current.commentDate = Convert.ToDateTime(dr["commentDate"]);
          current.email = Convert.ToString(dr["email"]);
          current.userID = Convert.ToInt16(dr["userID"]);
          comments.Add(current);
     }
     dr.Dispose();

     content += "<div style='width: 72%;'>";
     foreach (comment current in comments)
     {
          if (current.parentComment == 0)
          {
               content += mapcomment(current, comments);
          }
     }
     content += "</div>";
     return content;
}

First we have the public struct comment. What is this? Well, within the brackets you just see a bunch of variables, and that's basically what this is... a collection of variables. This is a structure, which is basically a bunch of variables rolled together into a container. I've designed this structure to hold a comment. You'll see how it works as we move along.

Next we have the comments() method. The first section is just a SQL query with which I retrieve the comments for the specified blog, ordered by date. Within the dr.Read() loop, you can see the first line is comment current = new comment(). The structure looks very similar to how you initialize a new ArrayList (done just outside of the dr.Read() loop). What I'm doing is creating a new comment structure called current. I'm then getting the SQL query results and assigning them to this structure. Finally, at the end of the loop I add them into an arraylist. This lets me take any number of comments, put all their variables into a single container, then add the containers into this arraylist. This makes manipulating the data much, much easier.

Finally, I put in a div container, then loop through each comment in the arraylist. If the current comment has a parentComment of 0 (ie, it's not a child of another comment), I run a method called mapcomment(). Into that I pass the current comment, as well as the entire arraylist of comments.
Comments (0)
This is where the magic is. Here's that final method.
private string mapcomment(comment current, ArrayList comments)
{
     string content = @"<div class='commentContainer'>
          <div class='comment'>
               <div class='header'>"
;
     content += (current.userID > 0) ? current.displayName : current.name;
     if (current.email.Length > 0)
     {
          content += current.email;
     }
     content += " @ " + current.commentDate.ToString("hh:mmtt ddMMMyyyy");
     if (accessLevel == 255)
     {
          content += " " +
               "<a href='default.aspx?option=editComment&commentID=" + current.commentID + "''>Edit</a> / " +
               "<a href='default.aspx?option=deleteComment&commentID=" + current.commentID + "'>Delete</a>";
     }
     content += @"</div>
               <div class='text'>"
+ current.commentText.Replace("\n", "<br>") + @"</div>
               <div class='reply'><a href='default.aspx?option=postComment&blogID="
+ current.blogID + "&commentID=" + current.commentID + @"'>Reply to Comment</a></div>
          </div>"
;
     foreach (comment loop in comments)
     {
          if (current.commentID == loop.parentComment)
          {
               content += mapcomment(loop, comments);
          }
     }
     content += "</div>";
     return content;
}

In the first part of this method, we take the current comment that was passed in and build the comment HTML. We display the displayName or name depending on if the comment is associated with a userID or not. We also add the option to edit and delete the comment for admins. Additionally, we have the option to reply to the comment. We'll write the reply, edit and delete methods later.

Now look at the foreach loop. We loop through all the comments again and look for any comment that has a parentComment field equal to this comment's id. If so, we run this same method again. That means we spit out the html for the child comment, then loop through all the comments again looking for another comment that's a child to this comment. This can go on forever... there could be hundreds of comments with children under children under children and we would just keep executing this method until we ran through them all. This is a recursive method and may be a bit difficult to wrap your head around at first. Keep poking at it, though... it's well worth figuring out how this works.

And at this point we have our comment displaying code completely done. Grab the files below to see where we are, then continue on for adding, editing and deleting comments.

Files To This Point
Comments (0)