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.
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.
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); }
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));
}
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.
Attributes.cs
file called RemoteAttribute
.Helpers/Attributes.cs:
public class RemoteAttribute : ValidationAttribute { }
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; } }
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);
}
Person
class and make the following amendment.Models/Person.cs:
[DataType(DataType.EmailAddress), Required, Email, Remote(Url = "/home/emailcheck")]
public string Email { get; set; }
Helpers
folder called RemoteAttributeAdapter
, which will inherit from DataAnnotationsModelValidator<RemoteAttribute>
.Helpers/RemoteAttributeAdapter.cs:
public class RemoteAttributeAdapter : DataAnnotationsModelValidator<RemoteAttribute> { }
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) { } }
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 };
}
}
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
.
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. 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; } }
__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; }
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; }
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.