Merging ModelState validation with Knockout models

Scenario: you have a server-side object in .NET, which you then serialize across to a client-side Knockout view model. Knockout supplies validation on the client-side, but you have one or two rules that you want to enforce on the server and only on the server, and for whatever reason you don’t want to run these asynchronously.

So let’s assume that you’ve got something like this:

public class LoginModel
{
    public string Username { get; set; }
    public string Password { get; set; }
}

This maps to a Knockout model which looks a little like:

function loginModel() {
  this.Username = ko.observable('').extend({ required: true });
  this.Password = ko.observable('').extend({ required: true });
}

All good. Let’s say that your controller action does this:

public IActionResult Login(LoginModel model)
{
  if (!CheckPassword(model.Username, Model.Password))
    ModelState.AddModelError("Password", "Invalid password");

  if (ModelState.IsValid)
  {
    // continue
  }

  return HttpBadRequest(ModelState);
}

Now, in your UI code, you can use the following snippet the map the server-side validation errors into your client-side model:

/**
 * Applies errors returned via the .NET model state error collection to a Knockout
 * view model by matching property names.
 * @param koModel Knockout view model to bind to
 * @param modelState Model state response (usually returned as JSON from an API call)
 */
function applyModelStateErrors(koModel, modelState) {
    // loop all properties of the `modelState` object
    for (var x in modelState) {
        if (modelState.hasOwnProperty(x)) {
            // try to get a property of our KO object with the same name
            var koProperty = koModel[x];

            // we're only interested in KO observable properties
            if (koProperty && ko.isObservable(koProperty)) {
                var error = modelState[x];
                var message = "";

                // .NET returns errors as an array per-property, but we
                // can check the type just to be safe
                if (error instanceof Array) {
                    message = error.join(", ");
                } else if (typeof error == "string") {
                    message = error;
                } else {
                   message = error.toString();
                }

                // set the error state for this property
                koProperty.setError(message);
            }
        }
    }
}

Given this, you can catch the 400 error in your AJAX call, use the function above, and map the server-side errors into your local model.

Advertisements