JN
3 min read

MVC Create custom comment system in c# with javascript

#entity-framework#javascript#mvc#jquery#code-first#csharp#comments

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

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.

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


Leave a comment