Todd Lucas

Software developer, etc.

Client Side ModelState

ASP.NET MVC has good capabilities for handling model state validation. Making use of model attributes, such as Required, it's pretty easy to put together a validating form quickly. MVC 3 introduced unobtrusive validation, which enables client-side validation prior to form post. The HTML helpers render additional mark-up to transparently assist.

There has been a steady move over the past several years of applications relying more and more on JavaScript. Whereas you would often hear terms like progressive enhancement or graceful degradation, you're now more likely to hear single page app. Libraries like KnockoutJS and AngularJS are becoming very popular.

A middle-ground approach is to begin moving some server side flows to a more client-first approach. A good candidate for this migration is the simple form. Whereas a typical form might use post/redirect/get, a client-first approach might use a dialog with the embedded form rendered with a partial. If you attempt to move to a more client-first approach, you'll quickly find that much of the validation capability built into MVC is lost.

Web API 2

Another great recent advancement in the ASP.NET stack is Web API 2. One of the interesting methods defined for the ApiController base class is BadRequest.

[HttpPost]
public IHttpActionResult Post(CheckDatesModel model)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    ...
}

This method will return any ModelState errors to the client as JSON:

{
    "Message": "The request is invalid.",
    "ModelState": {
        "model.StartDate": [ "The Start Date field is required." ],
        "model.EndDate": [ "The End Date field is required." ]
    }
}

Unfortunately, there doesn't appear to be any built-in mechanism to support this data on the client. Regardless, we can make use of this data to do our own client side validation using existing structures. A great example of this is written about in ASP.NET Web API Validation. More on that later.

Server side

It would be great if we had a comprehensive approach that would allow us to use the Web API mechanism, or to use a similar mechanism on the MVC side. All we really need are two pieces. The client side piece, for which brette has shown us the way, and a server-side complement to the Web API mechanism for use with MVC. The latter can be accomplished pretty easily using an extension method on the ModelStateDictionary class. Here's an example, written in one line of code :)

public static class ModelStateDictionaryExtensions
{
    public static Dictionary<string, string[]> GetErrors(
        this ModelStateDictionary modelState, 
        string prefix)
    {
        return modelState
            .Where(kvp => kvp.Value.Errors.Count > 0)
            .ToDictionary(kvp => String.IsNullOrWhiteSpace(kvp.Key)
                                ? String.Empty 
                                : prefix == null 
                                    ? kvp.Key
                                    : prefix.TrimEnd('.') + "." + kvp.Key,
                            kvp => kvp.Value.Errors
                                    .Select(e => e.ErrorMessage)
                                    .ToArray());
    }
}

To use the extension method from MVC, we would do something like this:

[HttpPost, ValidateAntiForgeryToken]
public ActionResult CheckDatesJson(CheckDatesModel model)
{
    if (!ModelState.IsValid)
    {
        Response.StatusCode = 400;
        return Json(new { 
            Message = "The request is invalid.",
            ModelState = ModelState.GetErrors("model") 
        });
    }
    ...
}

This produces the identical format as the equivalent Web API method:

[HttpPost]
public IHttpActionResult Post(CheckDatesModel model)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    ...
}

It would be pretty easy to wrap the Json and GetError calls into a method on a base class derived from Controller. One might even call it BadRequest. Although, hopefully, this new version would have a way to override the default Message.

Client side

The client side is a bit more complicated. It involves taking the JSON result and applying it to the existing validation mark-up.

Validation Summary

One of the first pieces to address is the Html.ValidationSummary() method. This method emits different markup depending on how it's called. The default Html.ValidateSummary() will render all errors, both model-level and errors that are not associated with a model. Other versions allow for the suppression of model-level errors by passing true for the excludePropertyErrors argument. These two versions render different HTML. However, this can be accommodated-for on the client.

In the default case, Html.ValidationSummary() renders this on initial form render, with the data-valmsg-summary="true" attribute.

<div class="validation-summary-valid" data-valmsg-summary="true">
    <ul>
        <li style="display:none"></li>
    </ul>
</div>

After the form post, if there are errors, they will be listed. In addition, the class will change from validation-summary-valid to validation-summary-errors.

One case that is problematic is when true is passed for excludePropertyErrors. When Html.ValidateSummary(true) is specified, no HTML is emitted on the initial form render. After a form post with errors, the result is the same as the default case, but without the data-valmsg-summary="true" attribute.

One solution would be to add a wrapper element which will always be present. This is the approach taken here, although it's optional.

@using (Html.BeginForm()) 
{
    @Html.AntiForgeryToken()
    <div class="validation-summary">
        @Html.ValidationSummary(true)
    </div>
    ...
}

This wrapper element allows us to construct validation error mark-up regardless of whether it was emitted by Html.ValidationSummary() or not.

Ajax and Render

The last step is to make the Ajax call.

<script src="~/scripts/app/modelstate.js"></script>

<script>
$(function () {
    var jForm = $("form");

    jForm.submit(function (event) {
        event.preventDefault();

        // Clear any previous errors.
        App.ModelState.clearErrors(jForm);

        $.ajax({
            url: '/mycontroller/checkdatesjson',
            data: $(this).serializeArray(),
            type: 'POST',
            success: function (data) {
                // Do something
            },
            statusCode: {
                400: function (jqXHR) {
                    // Deserialize and render the ModelState.
                    App.ModelState.showResponseErrors(jForm, jqXHR);
                }
            }
        });
    });
});
</script>

The implementation, based on brette's, is written in TypeScript. It can be found on GitHub here.

Applicability

This method of moving halfway to client-side processing is best used in places where you still want to use the great features of MVC, but want more client-side interactivity. By using this component, any new client-side form handling can have the same presentation as your existing server-side form handling, allowing for a consistent user experience.

blog comments powered by Disqus