MVC Create custom comment system in c# with javascript

by John Nye

29 Nov
2013

I recently created a custom blog engine for this blog and thought I'd share a simple overview of how I did it.

Demo: Simple validation demo to show custom comment validation

Creating the data models

In order to persist our comments in a data source, and retrieve them back, we need to define a data model that defines the information we require:

public class Comment
{
    [Required]
    public int Id { get; set; }

    [StringLength(50)]
    public string Author { get; set; }

    [Required]
    public string Body { get; set; }

    [StringLength(100)]
    public string Email { get; set; }

    [Required]
    public int PostId { get; set; }

    // Link to existing Post class 
    public virtual Post Post { get; set; }
}

I want to be able to access comments from a post so I also need to update my post model as follows:

public class Post
{
    // Current Properties...

    // New relationship property
    public virtual ICollection<Comment> Comments { get; set; }
}

Now I have my data models I simply need to create a data migration and update my database using the following commands

Add-Migration CreateComments -ProjectName Blog.Data -Verbose Update-Database -ProjectName Blog.Data -Verbose

Now that our database is up to date you may need to implement some code in order to access your data, be it a repository or otherwise.

Creating the view model

Now that the data can be persisted and retrieved from our datasource we can concentrate on how we allow users to create a comment. Firstly we need to create a viewmodel to represent a comment:

public class CommentViewModel
{
    //Represents a post id
    [Required]
    public int Id { get; set; }
    [Required]
    [StringLength(50)]
    public string Author { get; set; }
    [Required]
    [AllowHtml]
    public string Body { get; set; }
    [Required]
    [StringLength(100)]
    [DataType(DataType.EmailAddress)]
    [EmailAddress]
    public string Email { get; set; }
}

Creating the HttpPost action

We also need to create a post action in our controller to accept the comment form submissions. This will be a simple MVC post action like any other:

[HttpPost]
public ActionResult Comment(CommentViewModel viewModel)
{
    if (ModelState.IsValid)
    {
        //Mapping code - alternatively try AutoMapper
        var dataComment = new Comment();
        dataComment.PostId = viewModel.Id;
        dataComment.Author = viewModel.Author;
        dataComment.Body = viewModel.Body;
        dataComment.Email = viewModel.Email;

        // Create comment and save changes
        commentRepository.Create(comment);
        commentRepository.SaveChanges();

        return new EmptyResult();
    }

    var modelErrors = this.BuildModelErrors();
    // return a bad request to signify that adding the comment failed
    HttpContext.Response.StatusCode = 400;
    // return errors as Json, read by javascript
    return Json(modelErrors);
}

/// <summary>
/// Build a list of model errors from model state.  
/// This method flattens the model state errors.
/// </summary>
/// <returns>A list of Keys and Error messages</returns>
private List<ModelError> BuildModelErrors()
{
    var modelErrors = new List<ModelError>();
    var erroneousFields = this.ModelState.Where(ms => ms.Value.Errors.Any())
                                         .Select(x => new {x.Key, x.Value.Errors});

    foreach (var erroneousField in erroneousFields)
    {
        var fieldKey = erroneousField.Key;
        var fieldErrors = erroneousField.Errors.Select(error => 
                                            new ModelError(fieldKey, error.ErrorMessage));
        modelErrors.AddRange(fieldErrors);
    }
    return modelErrors;
}

//Class to hold model errors and the corresponding field key
private class ModelError
{
    public ModelError(string key, string errorMessage)
    {
        Key = key;
        ErrorMessage = errorMessage;
    }

    public string Key { get; set; }
    public string ErrorMessage { get; set; }
}        

If the view model fails validation I return a HttpStatusCodeResult with a HttpStatusCode.BadRequest (400) status code. This will ensure that our response is not treated as a success in our javascript, but more on that further down

Creating the form

We now need to create a form that can be used to create a comment. I have created this as a partial view so that I can add it easily to multiple pages

I could have simply created a simple Html.BeginForm() or Ajax.BeginForm() but I wanted a custom implementation with cleaner html so I decided to use plain html and javascript with a lot of help from jquery.

@model Blog.Models.Comments.CommentViewModel

<div class="comment-form-container">
    <form class="comment-form" data-action="@Url.Action("Comment", "Comments")">
        @Html.HiddenFor(m => m.Id)
        <div class="comment-body">
            <div>@Html.LabelFor(m => m.Author)</div>
            @Html.TextBoxFor(m => m.Author, new { Class = "comment-name" })
        </div>
        <div>
            <div>@Html.LabelFor(m => m.Email)</div>
            @Html.TextBoxFor(m => m.Email, new { Class = "comment-email" })
        </div>
        <div>
            <div>@Html.LabelFor(m => m.Body)</div>
            @Html.TextAreaFor(m => m.Body, new { Class="comment-body", rows="3", cols="50" })
        </div>
        <div class="comment-result" style="display: none;" >
            <span class="comment-result-text">An error occurred</span>
        </div>
        <div>
            <button type="submit" class="comment-form-submit">Submit comment</button>
        </div>
    </form>
</div>

Now we have a form we simply need to hook up the form submission

The javascript

Below is some javascript that can be used to post our form to the controller in order to submit a form:

$('.comment-form-container').on('click', '.comment-form-submit', function(e) {
    // prevent form submission
    e.preventDefault();
    var form = $(this).closest('form');
    var resultMessage = $('.comment-result', form);
    resultMessage.hide();

    var submitButton = $(this);
    // disable the submit button to stop 
    // accidental double click submission
    submitButton.attr('disabled', 'disabled');
    var resultMessageText = $('.comment-result-text', form);

    // client side validation
    if (validateComment(form, resultMessageText) == false) {
        resultMessage.addClass('comment-result-failure');
        resultMessage.fadeIn();
        // Re-enable the submit button
        submitButton.removeAttr('disabled');
        return;
    }

    var postUrl = form.data('action');
    var postData = form.serialize();

    $.post(postUrl, postData)
        .done(function(data) {
            resultMessage.addClass('comment-result-success');
            resultMessageText.html('Comment created successfully!');
            // Clear submitted value
            $('.comment-body', form).val('');
        })
        .fail(function() {
            resultMessage.addClass('comment-result-failure');
            resultMessageText.html('An error has occurred');
        })
        .always(function() {
            resultMessage.fadeIn();
            submitButton.removeAttr('disabled');
        });
});

I have also added some simple validation to check, client side, that required fields have values. This is in place to simply prevent unnecessarily posting back to the server if we already know a required field hasn't been given a value.

// validate required fields
var validateRequired = function (input) {
    if (input.val().length == 0) {
        input.addClass('input-validation-error');
        return false;
    }
    return true;
};

var validateComment = function (commentForm, resultMessageContainer) {
    var isValid = true;
    var errorText = '';
    var name = $('.comment-name', commentForm);
    if (!validateRequired(name)) {
        errorText += 'The Name field is required.<br />';
        isValid = false;
    }

    var body = $('.comment-body', commentForm);
    if (!validateRequired(body)) {
        errorText += 'The Body field is required.<br />';
        isValid = false;
    }

    var email = $('.comment-email', commentForm);
    if (!validateRequired(email)) {
        errorText += 'The Email field is required.<br />';
        isValid = false;
    }

    if (isValid == false) {
        resultMessageContainer.html(errorText);
    }

    return isValid;
};  

In the following post I will show how we can leverage jquery validate to perform the client side validation for us automatically to replace our custom validation.

Don't forget to check out the demo

Demo: Simple validation demo to show custom comment validation

Any comments, please let me know using the form below.

Comments 18

Dean says: 1449 days ago

Your comment count is missing a :hover color :-p

Nice blog mate :-)

mukesh says: 1288 days ago

hello john nye, where r u displaying comments after being submitted...?

John says: 1284 days ago

Hi Mukesh,

The Demo page doesn't display the comments as it's purpose is more to demonstrate the validation. If validation is passed and the server receives the comment, how you handle and display the comment is up to you.

The same system is used on this blog and I use the data posted to create a new comment entry instead of receiving new data.

Hope this helps

Brian says: 1238 days ago

Ideally or in today's WWW you want to be able to edit comments and delete comments using AJAX. That is where things can get tricky IMO. Having comments in textbox containers in a forloop was my first attempt, but it is just not clean enough for me. What do you suggest for html and how could I pass the comment's ids for AJAX? Thanks in advance

Brian says: 1238 days ago

Oh, I forgot if anyone is interested in attempting to create commenting section using knockout.js this is a great tutorial but once again uses texbox/textarea http://techbrij.com/facebook-wall-posts-comments-knockout-aspnet-webapi

John says: 1235 days ago

Hi Brian,

Editing becomes a bit more tricky but I do currently have the delete functionality on my blog site. Basically, I have created a new action result in controller called Delete

[HttpPost]
public ActionResult Delete(int id)
{
    this.commentRepository.Delete(id);
    this.commentRepository.SaveChanges();
    return new EmptyResult();
}

This coupled with the following javascript

$(function () {
    // Setup on click delete action across all comments
    $(&#39;.blog-comments&#39;).on(&#39;click&#39;, &#39;.delete-comment&#39;, function () {
        // find the related post html
        var post = $(this).closest(&#39;.blog-comment&#39;);
        // Get the post id from the post data attribute
        var postData = { id: post.data(&#39;post-id&#39;) };

        // Make a request to delete the post
        $.post(&#39;/posts/delete&#39;, postData)
            .always(function () {
                // Remove the post html from page gracefully
                post.slideUp(function () {
                    post.remove();
                });
            });
    });
})

Hope this helps.

Furkan says: 527 days ago

Nice!

robin says: 451 days ago

nice good

Meee says: 433 days ago

Niceee

sedat says: 303 days ago

very good

nvm says: 208 days ago

Nice

Tony says: 123 days ago

Amazing

brad says: 109 days ago

very good

saira says: 94 days ago

wow

SirLucky says: 80 days ago

Its actually something like this i want for my site

za says: 28 days ago

Hi

XYz says: 17 days ago

Nice

Bob says: 14 days ago

<b>thanks</b> for <b>sharing</b>

Leave a message...

18 Nov
2017