Remote validation with jQuery

In a lot of cases, validation is a static formula, something definable like a formula or regular expression. Sometimes however, validation requires an external source (like a database). In cases like this, putting the logic in an attribute is possible but not always ideal. And what happens to the client-side? It's not so easy to tell the browser to connect to a database before allowing a form submission. Well, it's actually a lot easier than I thought.

Getting ready

In this recipe, we'll think of our form as a registration form. We've been getting a lot of duplicate registrations, so we'd like to prevent users from registering an e-mail address more than once.

How to do it...

  1. Start from our last recipe and open up the home controller.
  2. Add the following method called IsEmailAlreadyRegistered to the top of the home controller class. This method will act as a list of e-mails that have already been registered. You will need to add a reference to the System.Linq namespace.
    Controllers/HomeController.cs:
    private bool IsEmailAlreadyRegistered(string email) {
    return new string[] { "[email protected]", "[email protected]", "[email protected]" }.Contains(email);
    }
    
  3. Now we need to add some code to the Index action (the one that accepts only POSTs) to validate against our list of e-mail addresses. Before we attempt to validate against our e-mail list, we'll make sure that the rest of the model has validated successfully. If the e-mail does already exist, we'll add an error message to the ModelState object. Add the following code after the TryUpdateModel call.
    Controllers/HomeController.cs:
    if (ModelState.IsValid && IsEmailAlreadyRegistered(person.Email)) {
    var propertyName = "email";
    ModelState.AddModelError(propertyName, "Email address is already registered");
    ModelState.SetModelValue(propertyName, ValueProvider.GetValue(propertyName));
    }
    

    Note

    The ModelState object represents the state of the models before they're sent to the view. The model state contains the validation message and the current values. If you provide a model error without a model value, you can run into unexpected errors further down the line.

  4. Let's see how this works. Build and run the project.
    How to do it...
  5. We should now be preventing duplicate registrations on the server, but ideally, we'd like this on the client as well. We'll do this by using remote validation, which generally involves an Ajax call to the server. First, we need to tell ASP.NET what remote validation is. Let's create a new attribute in our Attributes.cs file called RemoteAttribute.
    Helpers/Attributes.cs:
    public class RemoteAttribute : ValidationAttribute { }
    
  6. jQuery's validation plug-in will need a URL to connect to, in order to establish whether the e-mail has already been registered or not; so we'll add a URL property. Also, because this attribute is not going to be validating on the server, we'll override the IsValid method to always return true.
    Helpers/Attributes.cs:
    public class RemoteAttribute : ValidationAttribute {
    public string Url { get; set; }
    public RemoteAttribute() {
    ErrorMessage = "Email address is already registered";
    }
    public override bool IsValid(object value) {
    return true;
    }
    }
    
  7. Now before we go and decorate our Person model with our shiny new attribute, we need a URL for our client validation to validate against. Go back to the home controller and create a new action called EmailCheck.
    Controllers/HomeController.cs:
    public ActionResult EmailCheck(string email) {
    return Json(!IsEmailAlreadyRegistered(email), JsonRequestBehavior.AllowGet);
    }
    
  8. We can now decorate our model. Open up the Person class and make the following amendment.
    Models/Person.cs:
    [DataType(DataType.EmailAddress), Required, Email, Remote(Url = "/home/emailcheck")]
    public string Email { get; set; }
    
  9. At this stage we have an attribute, a URL, and we've decorated our model. Now we need an attribute adapter. Create a new class in your Helpers folder called RemoteAttributeAdapter, which will inherit from DataAnnotationsModelValidator<RemoteAttribute>.
    Helpers/RemoteAttributeAdapter.cs:
    public class RemoteAttributeAdapter : DataAnnotationsModelValidator<RemoteAttribute> { }
    
  10. DataAnnotationsModelValidator<> doesn't have a parameter-less constructor, so we'll need to create a new constructor for our class.
    Helpers/RemoteAttributeAdapter.cs:
    public class RemoteAttributeAdapter : DataAnnotationsModelValidator<RemoteAttribute> {
    public RemoteAttributeAdapter(ModelMetadata metadata, ControllerContext context, RemoteAttribute attribute) : base(metadata, context, attribute) { }
    }
    
  11. I mentioned in the last recipe that the adapter had its part to play in what was rendered on the client; this happens in a method called GetClientValidationRules. You'll see from the following code that we can add custom parameters to relate to our new attribute.

    Helpers/RemoteAttributeAdapter.cs:

    public class RemoteAttributeAdapter : DataAnnotationsModelValidator<RemoteAttribute> {
    public RemoteAttributeAdapter(ModelMetadata metadata, ControllerContext context, RemoteAttribute attribute) : base(metadata, context, attribute) { }
    public override IEnumerable<ModelClientValidationRule> GetClientValidationRules() {
    var rule = new ModelClientValidationRule {
    ErrorMessage = ErrorMessage,
    ValidationType = "remote"
    };
    rule.ValidationParameters["url"] = Attribute.Url;
    rule.ValidationParameters["type"] = "get";
    return new[] { rule };
    }
    }
    

    Note

    GetClientValidationRules returns a collection of ModelClientValidationRules. These rules are special because when serialized, they make up the structure of the JSON map that we identified two recipes ago, called mvcClientValidationMetdata.

  12. In the last step, we created a rule with a validation type of remote. Remote validation is baked straight into the jQuery validation plug-in—the Microsoft connector (MicrosoftMvcJQueryValidation.js) however, has no idea what remote validation is. So, we will need to make some changes. Open up the MicrosoftMvcJQueryValidation.js in your Scripts folder.
  13. The connector file is a series of functions that respond to different validation types. Our first job will be to create a function to respond to the remote validation type (or rule). When you look at the following code, you will see that it acts like a mirror for the GetClientValidationRules method that we created a second ago. It consumes the custom parameter's set on the server and produces a jQuery-friendly options object.
    Scripts/MicrosoftMvcJQueryValidation.js:
    function __MVC_ApplyValidator_Remote(object, validationParameters, fieldName) {
    var obj = object["remote"] = {};
    var props = validationParameters.additionalProperties;
    obj["url"] = validationParameters.url;
    obj["type"] = validationParameters.type;
    if (props) {
    var data = {};
    for (var i = 0, l = props.length; i < l; ++i) {
    var param = props[i];
    data[props[i]] = function () {
    return $("#" + param).val();
    }
    }
    obj["data"] = data;
    }
    }
    
  14. We're almost there. We just need to connect the remote validation type to our function, as well as pass the correct parameters. Our new function makes use of the name of the field being validated. The factory method (__MVC_CreateRulesForField) that associates the validation type to the function currently has no knowledge of this, so we'll need to pass it in as an additional parameter.
    Scripts/MicrosoftMvcJQueryValidation.js:
    function __MVC_CreateValidationOptions(validationFields) {
    var rulesObj = {};
    for (var i = 0; i < validationFields.length; i++) {
    var validationField = validationFields[i];
    var fieldName = validationField.FieldName;
    rulesObj[fieldName] = __MVC_CreateRulesForField(validationField, fieldName);
    }
    return rulesObj;
    }
    
  15. Now we want to tell the factory method to make use of the additional parameter, and apply it to our new function.
    Scripts/MicrosoftMvcJQueryValidation.js:
    function __MVC_CreateRulesForField(validationField, fieldName) {
    var validationRules = validationField.ValidationRules;
    // hook each rule into jquery
    var rulesObj = {};
    for (var i = 0; i < validationRules.length; i++) {
    var thisRule = validationRules[i];
    switch (thisRule.ValidationType) {
    case "range":
    __MVC_ApplyValidator_Range(rulesObj, thisRule.ValidationParameters["minimum"], thisRule.ValidationParameters["maximum"]);
    break;
    case "regularExpression":
    __MVC_ApplyValidator_RegularExpression(rulesObj, thisRule.ValidationParameters["pattern"]);
    break;
    case "required":
    __MVC_ApplyValidator_Required(rulesObj);
    break;
    case "stringLength":
    __MVC_ApplyValidator_StringLength(rulesObj, thisRule.ValidationParameters["maximumLength"]);
    break;
    case "remote":
    __MVC_ApplyValidator_Remote(rulesObj, thisRule.ValidationParameters, fieldName);
    break;
    default:
    __MVC_ApplyValidator_Unknown(rulesObj, thisRule.ValidationType, thisRule.ValidationParameters);
    break;
    }
    }
    return rulesObj;
    }
    
  16. If we build and run this form in Firefox (with FireBug switched on), we should be able to see our remote validation in action.
    How to do it...

How it works...

The validation plug-in of jQuery is a comprehensive collection of rules, which can be triggered in any number of ways. The most common approach is through the use of class names applied to the input field; where a textbox decorated with the class required would result in the surrounding form not being able to submit without the said textbox being filled in. Another way to initiate the validation plug-in is through the use of an options object. The options object is a schema for how the form should be validated.

ASP.NET MVC's client-side validation was not built with jQuery's specific schema in mind, so the MicrosoftMvcJQueryValidation.js file was written to bridge the JavaScript injected by ASP.NET MVC (mvcClientValidationMetdata) with the jQuery options object, then initializing the validation plug-in.

Scripts/MicrosoftMvcJQueryValidation.js:
function __MVC_EnableClientValidation(validationContext) {
// this represents the form containing elements to be validated
var theForm = $("#" + validationContext.FormId);...var options = {
errorClass: "input-validation-error",...$(messageSpan).removeClass("field-validation-error");}
};
// register callbacks with our AJAX system
var formElement = document.getElementById(validationContext.FormId);
var registeredValidatorCallbacks = formElement.validationCallbacks;
if (!registeredValidatorCallbacks) {
registeredValidatorCallbacks = [];
formElement.validationCallbacks = registeredValidatorCallbacks;
}
registeredValidatorCallbacks.push(function () {
theForm.validate();
return theForm.valid();
});
theForm.validate(options);
}

Baked into jQuery's validation plug-in is this concept of remote validation. Remote validation is validation where not all the variables are known at implementation, so there is a requirement to source that information from a remote location. When dealing with remote validation it is preferable to try and retrieve any information without disrupting the user's workflow; for this reason we use an Ajax request to the server. As the user types in his/her e-mail address, the client-side script silently fires off requests to our URL (or endpoint) to unobtrusively validate the input.

We enabled jQuery's remote validation functionality by passing through the URL of an action, which will return a Boolean based on the existence of the entered e-mail within our dummy method. jQuery took care of the rest with a simple GET request to our endpoint.

Validation was something I didn't particularly enjoy in ASP.NET web forms, but now relish in ASP.NET MVC. It is another great example of the extensibility of the new framework.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset