Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Integrating Client & Server-Side Validation w/ Bootstrap Modals In ASP.NET MVC5

0.00/5 (No votes)
6 Dec 2015 1  
A simple how-to for beginners to .NET MVC for the implementation of bootstrap modals and client/server-side validation.

Introduction

This article aims to provide a basic tutorial on the implementation of Bootstrap modals and, more poignantly, a take on the server-side validation that you will want to implement also. It builds upon a CodeProject article by Lyubomir Rumenov Velchev, located here.

As an inexperienced developer, I put a fair chunk of time into developing the validation technique, and I am hoping that this article can help both newbies looking to implement something similar, and that experienced coders might have a look and help me improve the code.

Background

When building a default MVC 5 project (I am using Visual Studio 2015), it likes to make use of seperate pages for each CRUD function, which is somewhat antiquated with today's new, responsive, websites. I wanted to take these functions ("Create", "Edit", "Delete") and allow the user to work with them from the basic Index page, rather than have to change pages.

In following the article referenced above, this was fairly easy. However, it took me some work and time to figure out the next steps to properly implement a form with the validation that you want. I quickly realized that to do it properly, three things needed to change immediately:

  1. I needed to change the form to make use of Ajax (specifically, the Ajax.BeginForm() helper method;
  2. The controller method needed to be changed to return JSON instead of a ViewResult in order to retrieve the ModelState errors that would normally be returned with the view; and
  3. JavaScript needed to be implemented to report on the server-side validation errors passed back as JSON from the controller

The remainder of this article assumes you have read and understand Mr Velchev's article so please do go and review carefully. I will be making a couple of points (below) and then will be focussing on the integration of the validation technique that I used.

Implementation

1. Modify the View to Use Ajax.BeginForm() Helper

First, the form within the modal (within the content in the partial view) needed to be changed to make use of the Ajax.BeginForm() helper:

@using (Ajax.BeginForm("Create", "UsersAdmin", null, new AjaxOptions
                {
                    HttpMethod = "POST",
                    InsertionMode = InsertionMode.Replace, 
                    OnSuccess = "formReturn(data)",
                    OnFailure = "error(data)"
                }))
The overload of the method looks like this: Ajax.BeginForm("Action", "Controller", "RouteValues", "AjaxOptions").
 
Important in the AjaxOptions are the OnSuccess and OnFailure properties. These specify the JavaScript functions that will be called once the controller returns the JSON data - aka the data parameter that each function is told to receive.
 
From there the rest of the form is the same as your typical Html.BeginForm helper would generate:
{
    @Html.AntiForgeryToken()
    <div class="form-horizontal">
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <div id="validation-summary" class="text-danger" hidden="hidden">
            <ul></ul>
        </div>
        <div class="form-group">
            @Html.LabelFor(model => model.FirstName, new { @class = "control-label col-md-3" })
            <div class="col-md-9">
                @Html.TextBoxFor(m => m.FirstName, new { @class = "form-control" })
                @Html.ValidationMessageFor(model => model.FirstName, "", new { @class = "text-danger" })
            </div>
        </div>
        <div class="form-group">
            @Html.LabelFor(model => model.Email, new { @class = "control-label col-md-3" })
            <div class="col-md-9">
                @Html.TextBoxFor(m => m.Email, new { @class = "form-control" })
                @Html.ValidationMessageFor(model => model.Email, "", new { @class = "text-danger" })
            </div>
        </div>
        <div class="form-group">
            @Html.LabelFor(model => model.ConfirmEmail, new { @class = "control-label col-md-3" })
            <div class="col-md-9">
                @Html.TextBoxFor(m => m.ConfirmEmail, new { @class = "form-control" })
                @Html.ValidationMessageFor(model => model.ConfirmEmail, "", new { @class = "text-danger" })
            </div>
        </div>
        <div class="modal-form-buttons">
            <button type="button" class="btn btn-danger" data-dismiss="modal">
                <i class="fa fa-thumbs-o-down fa-lg confirm-button-thumbs"></i>
            </button>
            <button type="submit" value="Create" id="approve-btn" class="btn btn-primary">
                <i class="fa fa-plus fa-lg confirm-button-thumbs"></i>
            </button>
        </div>
    </div> <!-- /.form -->
}

2. Modify the Controller to Return JSON to the Ajax Call

Now the controller needs to be changed to return JSON instead of views - mainly to return the ModelState errors which would normally come back with the returned view. Typically, your controller would look something like:

[HttpPost]
public ActionResult Create (CreateViewModel model)
{
    if (ModelState.IsValid)
    {
        // Create your thing then return to a different view
    }
    else
    {
        return View(model);
    }
}

The model that gets returned in the view in the else statement contains the ModelState errors that have somehow snuck into the model. We want to capture these and return them to the same view that we have aready loaded. 

To do this we should write a method that can be used by other methods:

//
// Public: Serializes the Key/Value pairs in ModelState errors and returns a Json object
public JsonResult ModelStateErrorSerializer (ModelStateDictionary modelState)
{
    // Initialize Dictionary for holding ModelState errors then loop through and add
    Dictionary<string, string> errors = new Dictionary<string, string>();

    foreach (var item in modelState)
    {
        foreach (var error in item.Value.Errors)
        {
            errors.Add(item.Key, error.ErrorMessage);
        }
    }
    // Return the serialized object
    return Json(errors, JsonRequestBehavior.AllowGet);
}

As it describes the method accepts your controller action's ModelState object which, buried within, contains a list of key->value pairs which we want to serialize into JSON. We would call this from our controller per the following:

else
{
    var errors = ModelStateErrorSerializer(ModelState);
    ...
}

Note: for my project, I wanted to be able to call this method from other controllers, and so I created a new controller - "BaseController" - which I then inherit into my other controllers that want to use this method. I can then add other "global" methods to this base controller.

Once we get this object back, it will contain a property called Data in which we would access our errors using errors.Data. We then want to return the Json to the view to complete the Ajax call:

else
{
    var errors = ModelStateErrorSerializer(ModelState);
    return Json(new { errors = errors.Data }, JsonRequestBehavior.AllowGet);
}

3. Handle the ModelState Errors Using JavaScript Back In the View

I wanted to take the server-side validation and include under the correct fields in the form (in spite of the fact that the client-side validation should pick this before submitting). I also knew there needed to be a place to include non-field-specific errors. I first implemented the latter by adding a hidden Div element to my partial view:

<div id="validation-summary" class="text-danger" hidden="hidden">
    <ul></ul>
</div>

I chose to put this right underneath the @Html.ValidationSummary(...) helper, which is where the JQuery Validate errors will be rendered.

Now we can get to building our formReturn(data) function which, if you remember, is our "Success" function from our Ajax Post call to the Create method. I have pasted my code below which I have commented line-by-line.

/*
 * Title: formReturn (Success Function for Create post Ajax call)
 * Description: Accepts JSON object from [HttpPost]Create method consisting
 * of all ModelState errors, then displays in default places in form.
 */
function formReturn(data) {
    // Null check on the data parameter (should never be null)
    if (data != null) {
        // Check which element has been returned from the controller (errors or Url for success)
        if (data.errors) {
            // Clear any errors already on the form
            $("span[data-valmsg-for]").text("");
            // Note: #validation-summary is a custom field added to the form as .NET's won't fire
            var $valSum = $('#validation-summary');
            $valSum.hide();
            // Access actual data  in object per controller "data = errors*.Data"
            var errors = data.errors;
            // Access all keys in object and determine length
            var keys = Object.keys(errors);
            var len = keys.length;
            // Loop over keys and select the span on the form which 
            // has the data-valmsg-for attribute set to the key (managed by .NET),
            // then set the value of the error message to the key's corresponding value.
            for (var i = 0; i < len; i++) {
                var $span = $('span[data-valmsg-for="' + keys[i] + '"]');
                // Check if the span doesn't exist...
                if ($span == undefined || $span == null || keys[i] == "") {
                    // ...then the error message is for something other than a field so display custom 
                    $('#validation-summary > ul').append("<li>" + errors[keys[i]] + "</li>");
                    $valSum.show();
                }
                else {
                    // Otherwise add the value (actual error message) to the span
                    $span.text(errors[keys[i]]);
                }
            }
        }
        else if (data.Url) {
            // If the redirect url came back then redirect to it
            window.location.href = data.Url;
        }
        else {
            // Otherwise something when wrong as nothing else should be returned
            RenderPartialInModal("_GeneralError");
        }
    }
    else {
        // 'data' should never be null from the Ajax call
        alert("Error: data parameter cannot be null.");
    }
}

You can see that we take receipt of the returned object from the controller method as our data parameter. We then check the errors parameter, and begin a process of looping over the errors, extracting the keys which are equal to the data-valmsg-for HTML tag.

You will see some additions that I have included:

  • else if (data.Url) ... - when there are no errors, I send back the Url of the "Details" ActionResult which I want to redirect the user to once they have created the user. I have not included the details of this as it is out of the scope of this article (but not by any means difficult).
  • else { RenderPartialInModal("_GeneralError") } - this is a seperate JavaScript function I wrote which works in conjunction with a controller method to convert a PartialView to a string for rendering within a modal. I use this in this case to pop up an error message for the user. This is the subject of another article, however.

Points of Interest

There was one main annoyance when implementing this related to the way MVC renders scripts.

The out-of-the-box jQuery client-side validation failed to fire in the modal, in spite of it being enabled in web.config, the jquery-val scripts being included in the correct order in the script bundles, and the script bundles being included in (and in the correct order) the _Layout view which houses the modal container. I found that to make this work, I needed to remove the JQuery Val bundle from the _Layout view, and render it in each partial view containing my forms. 

I initially thought I could just add it to the partial view (and leave it on the _Layout view), but this resulted in two seperate requests being made to the controller almost simultaneously. You could have resolved this dual-request issue by seperating out the bundles, but I think my approach made more sense.

History

This is the fist/initial copy of this article.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here