One validation feature that we didn’t quite have time for in MVC 2’s new client-side validation support is called “remote validation”.
Remote validation means we make a call back to our web site to ensure that a given value is valid. You might want this in cases where it’s not easy (or possible) to determine whether the value is valid on the client side, but you’d still like to give the user client-side feedback about the validity of the item. The canonical example is checking to see if a desired username is taken during your site registration process.
This is an example remote validator that I modified from an example by Levi Broderick (who did the bulk of the JavaScript work for client-side validation). This example was tested on MVC 2 RC, and assumes that you’re using the DataAnnotations validation system (the default).
In order to make this work, we will need to perform a couple of one-time steps:
- Include a JavaScript file that enables remote validation.
- Register a server-side adapter for remote validation.
We’ll also need to perform a couple of steps for each remote validation we want to perform:
- Write an action which accepts the value to be tested and returns a string, either “true” (the model is valid), “false” (the model is invalid and you want to use the default message), or any other string (the model is invalid, and the string that we return should be the error message).
- Put the [Remote] attribute to the model property that you want remotely validated, pointing to the action you just wrote.
The Client-Side Bits
When you want to write a client-side validator for MVC 2, you will write a function (in JavaScript) that registers the validator and performs the validation when prompted to. For remote validation, this is our example JavaScript:
Sys.Mvc.ValidatorRegistry.validators.remote = function(rule) { var url = rule.ValidationParameters.url; var parameterName = rule.ValidationParameters.parameterName; var message = rule.ErrorMessage; return function(value, context) { if (!value || !value.length) { return true; } if (context.eventName != 'blur') { return true; } var newUrl = ((url.indexOf('?') < 0) ? (url + '?') : (url + '&')) + encodeURIComponent(parameterName) + '=' + encodeURIComponent(value); var completedCallback = function(executor) { if (executor.get_statusCode() != 200) { return; } var responseData = executor.get_responseData(); if (responseData != 'true') { var newMessage = (responseData == 'false' ? message : responseData); context.fieldContext.addError(newMessage); } }; var r = new Sys.Net.WebRequest(); r.set_url(newUrl); r.set_httpVerb('GET'); r.add_completed(completedCallback); r.invoke(); return true; }; };
Put this in a .js file, and reference it in the same place where you’re already referencing MicrosoftAjax.js and MicrosoftMvcValidation.js (usually in your Site.Master file or some equivalent).
The first line sets up a new client-side validation rule named “remote”. The anonymous function sets up the rule, and returns a function which performs the validation. The function accepts the value and a context object we create for you which contains information about the actual validation you’re doing.
The first if block stops us from doing anything is there’s no value. Validators should always return success (true) when given an empty value, because it’s the “required” validator that’s responsible for denying empty values.
The second if block only runs our validation during the “blur” event (i.e., when the user tabs out of the field). The other two events are “input” (fired as the user types) and “submit” (fired when the user wants to submit the form). Neither of these are appropriate because “input” would happen too much, and “submit” is too late for a remote validation. One consequence of this is that, with this implementation, users will be allowed to submit values to the server where the remote validation might’ve failed. You could change this JS file to prevent that (stash the previous result from your “blur” event and return that during the “submit” event), but I’ll leave that as an exercise for the reader.
We append the value to the URL and submit a new GET request to the URL, and then hook the response to read the result, setting a validation error message if the response was anything other than “true”.
The Server-Side Bits
We’ll start with the RemoteAttribute class:
using System; using System.ComponentModel.DataAnnotations; using System.Web.Mvc; using System.Web.Routing; [AttributeUsage(AttributeTargets.Property)] public class RemoteAttribute : ValidationAttribute { protected RemoteAttribute(string parameterName) { ParameterName = parameterName; RouteData = new RouteValueDictionary(); } public RemoteAttribute(string action, string controller, string parameterName) : this(parameterName) { RouteData["controller"] = controller; RouteData["action"] = action; } public RemoteAttribute(string routeName, string parameterName) : this(parameterName) { RouteName = routeName; } public string ParameterName { get; protected set; } protected RouteValueDictionary RouteData { get; set; } protected string RouteName { get; set; } public virtual string GetUrl(ControllerContext controllerContext) { var pathData = RouteTable.Routes.GetVirtualPath(controllerContext.RequestContext, RouteName, RouteData); if (pathData == null) throw new InvalidOperationException("No route matched!"); return pathData.VirtualPath; } public override bool IsValid(object value) { return true; } }
This attribute derives from the DataAnnotations ValidationAttribute base class. There’s no server-side validation by default (the “return true” line in IsValid), though you could derive from this attribute to provide both server- and client-side validation. We’ll talk about this more in a bit.
There are two constructors: one where you provide action + controller + parameter name, and one where you provide route name + parameter name. ParameterName is the name of the parameter name of your action method which will receive the value to validate.
Here’s the RemoteAttributeAdapter class:
using System.Collections.Generic; using System.Web.Mvc; public class RemoteAttributeAdapter : DataAnnotationsModelValidator<RemoteAttribute> { public RemoteAttributeAdapter(ModelMetadata metadata, ControllerContext context, RemoteAttribute attribute) : base(metadata, context, attribute) { } public override IEnumerable<ModelClientValidationRule> GetClientValidationRules() { ModelClientValidationRule rule = new ModelClientValidationRule() { ErrorMessage = ErrorMessage, ValidationType = "remote" }; rule.ValidationParameters["url"] = Attribute.GetUrl(ControllerContext); rule.ValidationParameters["parameterName"] = Attribute.ParameterName; return new ModelClientValidationRule[] { rule }; } }
This a DataAnnotations validator adapter which enables the client-side validation support on the server side (say that 3x fast). The purpose of this class is to tell the DataAnnotations validation system what to emit into the client-side JavaScript when it finds one of these validator attributes. The values it's emitting are based on the JavaScript file we wrote above: a rule named remote, which has two parameters: url and parameterName.
You will need to register this adapter in your Global.asax file, in the Application_Start() method, by calling:
DataAnnotationsModelValidatorProvider.RegisterAdapter( typeof(RemoteAttribute), typeof(RemoteAttributeAdapter) );
Putting It All Together
Let's start with a model that uses remote validation:
using System.ComponentModel; using System.ComponentModel.DataAnnotations; public class RemoteModel { [Required] [DisplayName("Any Number")] public int AnyNumber { get; set; } [Required] [Remote("IsOdd", "Sample", "value", ErrorMessage = "Value for '{0}' is not odd.")] [DisplayName("Odd Number")] public int OddNumber { get; set; } }
And the controller:
using System; using System.Web.Mvc; public class SampleController : Controller { public string IsOdd(int value) { return ((value % 2) == 1) ? "true" : "false"; } public ActionResult Index() { return View(); } [HttpPost] public ActionResult Index(RemoteModel model) { return View(model); } }
And the view:
<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="ViewPage<RemoteModel>" %> <asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server"> <p> Server thinks the model <b><%= ViewData.ModelState.IsValid ? "IS" : "IS NOT" %></b> valid. </p> <% using (Html.BeginForm()) { %> <%= Html.EditorForModel() %> <input type="submit" /> <% } %> </asp:Content>
When we fire up the page, we’ll see two editor boxes (for “Any Number” and “Odd Number”). If we click submit, we will immediately see via client-side validation that the values are not valid because they are required.
You can tell it's client-side validation, because the browser “busy” icon never spins, the page doesn't flash, and the sentinel at the top will tell you that the server still thinks your model is valid. If it looks you're still posting to the server, make sure you’ve included all 3 JavaScript files as well as called <% Html.EnableClientValidation(); %> somewhere in your view or master page.
Now type “1” into “Any Number”, and “2” into “Odd Number”, then tab away to the “Submit Query” button (don’t click it yet). The remote validation will fire and quickly you’ll see an error next to “Odd Number” which says “Value for 'Odd Number' is not odd.”.
Now take note that you can still click Submit Query, and it will still submit up to the server (as I alluded to above). More interestingly, though, is that the error went away from next to “Odd Number”. That’s because our [Remote] attribute doesn’t do any server-side validation. We could re-write it to execute the entire action and get the result, but that just seems too heavy-weight.
Instead, let’s write a new attribute which derives from RemoteAttribute:
using System; [AttributeUsage(AttributeTargets.Property)] public class OddNumberAttribute : RemoteAttribute { public OddNumberAttribute() : base("IsOdd", "Sample", "value") { ErrorMessage = "{0} must be odd."; } public override bool IsValid(object value) { if (value == null) return true; int intValue; if (!Int32.TryParse(value.ToString(), out intValue)) return false; return IsOdd(intValue); } public static bool IsOdd(int value) { return ((value % 2) == 1); } }
Note that we've moved our “complex logic” into the OddNumberAttribute as a static. Normally we’d probably already have isolated this logic somewhere else in our class hierarchy, but this was a convenient place for it to in the sample. The important point is that this code is reachable both from within the new attribute, as well as from the controller, because we’re going to re-write the IsOdd action on our controller to call the same logic now:
public string IsOdd(int value) { return OddNumberAttribute.IsOdd(value) ? "true" : "false"; }
We update our model to use the new attribute instead of the old Remote attribute:
[Required] [OddNumber] [DisplayName("Odd Number")] public int OddNumber { get; set; }
And one last change to our registration code in global.asax:
DataAnnotationsModelValidatorProvider.RegisterAdapter( typeof(OddNumberAttribute), typeof(RemoteAttributeAdapter) );
Now back to our form, and this time when we try to submit a value with a non-odd number, the server will tell us we made a mistake. We'll get a round-trip and a flash of content, but now Odd Number will still be invalid, and our little sentinel on the top says that "Server thinks the model IS NOT valid." now.
That's it! Happy remote validating!