How to: Implement Remote Validation from a Client in MVC
ASP.NET MVC 2 provides a mechanism that can make a remote server call in order to validate a form field without posting the entire form to the server. This is useful when you have a field that cannot be validated on the client and is therefore likely to fail validation when the form is submitted. For example, many Web sites require you to register using a unique user ID. For popular sites, it can take several attempts to find a user ID that is not already taken, and the user's input is not considered valid until all fields are valid, including the user ID. Being able to validate remotely saves the user from having to submit the form several times before finding an available ID.
The following illustration shows a new-user form that is displaying an error message that indicates that the requested ID is not available. The ID that users enter is validated as soon as they leave the User Name text box (that is, when the text box loses focus). Validation does not require a full postback.
As an example of remote validation, this topic shows how to implement a form similar to the one in the previous illustration. The example can serve as a starting point to create your application-specific remote validation.
A Visual Studio project with source code is available to accompany this topic: Download.
Configuring Remote Validation on the Server
To configure remote validation on the sever
Create or open an ASP.NET MVC Web application project.
Note
The downloadable sample contains a Visual Basic project named MvcRemoteValVB and a Visual C# project named MvcRemoteValCS.
Create a folder in the project that can contain the validation classes. For example, create a folder named Validation.
Create a custom validation attribute that derives from ValidationAttribute.
If the validation attribute is intended to be used only with remote validation and only when an object is created (not when an object is edited), the overridden IsValid should return true.
The following example shows a custom validation attribute named RemoteUID_Attribute.
Public NotInheritable Class RemoteUID_Attribute Inherits ValidationAttribute Private _Action As String Public Property Action() As String Get Return _Action End Get Set(ByVal value As String) _Action = value End Set End Property Private _Controller As String Public Property Controller() As String Get Return _Controller End Get Set(ByVal value As String) _Controller = value End Set End Property Private _ParameterName As String Public Property ParameterName() As String Get Return _ParameterName End Get Set(ByVal value As String) _ParameterName = value End Set End Property Private _RouteName As String Public Property RouteName() As String Get Return _RouteName End Get Set(ByVal value As String) _RouteName = value End Set End Property Public Overloads Overrides Function IsValid(ByVal value As Object) As Boolean Return True End Function End Class
public sealed class RemoteUID_Attribute : ValidationAttribute { public string Action { get; set; } public string Controller { get; set; } public string ParameterName { get; set; } public string RouteName { get; set; } public override bool IsValid(object value) { return true; } }
The custom ValidationAttribute class lets you specify the action method name that is used for remote validation, the name of the validation controller, the name of the parameter to validate, and the name of the route. The route name parameter is used when you have multiple registered routes. (The example does not use the route parameter.) The derived class includes an override of the IsValid method.
Create a validation adapter class that has the following characteristics:
It should derive from the DataAnnotationsModelValidator<TAttribute> class.
It should implement a ModelValidator class that is associated with the custom ValidationAttribute class that you have created.
It should take the validation attribute described in the previous step as a type parameter.
It should override the base GetClientValidationRules method of the ModelValidator class and return the rules that implement your custom client-validation scenario.
Implement the validation adapter class described in the previous step that enables client-side validation support and that makes a remote call to the server.
The class specifies information that validator classes defined in the System.ComponentModel.DataAnnotations namespace use to generate JavaScript code for methods that are marked with the ValidationAttribute attribute. The values that the adapter class emits are based on a JavaScript file. (The contents of the file are described in the next section of this topic.) The GetClientValidationRules method you implement should return an array of ModelClientValidationRule instances. Each of these instances represents metadata for a validation rule that is written in JavaScript and that runs on the client. The array of ModelClientValidationRule instances is metadata and is converted to JSON-formatted information and sent to the client so that client validation can call the rules that are specified by the attribute.
The following example shows a validation adapter class named RemoteAttributeAdapter.
Public Class RemoteAttributeAdapter Inherits DataAnnotationsModelValidator(Of RemoteUID_Attribute) Public Sub New(ByVal metadata As ModelMetadata, ByVal context As ControllerContext, ByVal attribute As RemoteUID_Attribute) MyBase.New(metadata, context, attribute) End Sub Public Overloads Overrides Function GetClientValidationRules() As IEnumerable(Of ModelClientValidationRule) Dim rule As New ModelClientValidationRule() rule.ErrorMessage = ErrorMessage rule.ValidationType = "remoteVal" rule.ValidationParameters("url") = GetUrl() rule.ValidationParameters("parameterName") = Attribute.ParameterName Return New ModelClientValidationRule() {rule} End Function Private Function GetUrl() As String Dim rvd As New RouteValueDictionary() rvd.Add("controller", Attribute.Controller) rvd.Add("action", Attribute.Action) Dim virtualPath = RouteTable.Routes.GetVirtualPath(ControllerContext.RequestContext, Attribute.RouteName, rvd) If virtualPath Is Nothing Then Throw New InvalidOperationException("No route matched!") End If Return virtualPath.VirtualPath End Function End Class
public class RemoteAttributeAdapter : DataAnnotationsModelValidator<RemoteUID_Attribute> { public RemoteAttributeAdapter(ModelMetadata metadata, ControllerContext context, RemoteUID_Attribute attribute) : base(metadata, context, attribute) { } public override IEnumerable<ModelClientValidationRule> GetClientValidationRules() { ModelClientValidationRule rule = new ModelClientValidationRule() { // Use the default DataAnnotationsModelValidator error message. // This error message will be overridden by the string returned by // IsUID_Available unless "FAIL" or "OK" is returned in // the Validation Controller. ErrorMessage = ErrorMessage, ValidationType = "remoteVal" }; rule.ValidationParameters["url"] = GetUrl(); rule.ValidationParameters["parameterName"] = Attribute.ParameterName; return new ModelClientValidationRule[] { rule }; } private string GetUrl() { RouteValueDictionary rvd = new RouteValueDictionary() { { "controller", Attribute.Controller }, { "action", Attribute.Action } }; var virtualPath = RouteTable.Routes.GetVirtualPath(ControllerContext.RequestContext, Attribute.RouteName, rvd); if (virtualPath == null) { throw new InvalidOperationException("No route matched!"); } return virtualPath.VirtualPath; } }
Note
The RemoteAttributeAdapter and RemoteUID_Attribute classes shown in the previous example can be used without modification in many applications that must invoke validation remotely.
In the Application_Start method of the Global.asax file, call the RegisterAdapter method to register the attribute and the adapter.
The following example shows how to use the RegisterAdapter method to register the classes RemoteUID_Attribute and RemoteAttributeAdapter.
protected void Application_Start() { AreaRegistration.RegisterAllAreas(); RegisterRoutes(RouteTable.Routes); DataAnnotationsModelValidatorProvider.RegisterAdapter( typeof(RemoteUID_Attribute), typeof(RemoteAttributeAdapter)); }
Sub Application_Start() AreaRegistration.RegisterAllAreas() RegisterRoutes(RouteTable.Routes) DataAnnotationsModelValidatorProvider.RegisterAdapter( _ GetType(RemoteUID_Attribute), _ GetType(RemoteAttributeAdapter)) End Sub
Creating the Client-Side Validator
To implement a client-side remote validator for MVC, you write a JavaScript function that registers the validator and that performs the remote validation when you call the function.
To create the client-side validator
In the Scripts folder, create a new JScript (.js) file.
Add the following script to the new file, overwriting any code that is already in the file.
Sys.Mvc.ValidatorRegistry.validators.remoteVal = function(rule) { var url = rule.ValidationParameters.url; var parameterName = rule.ValidationParameters.parameterName; return function(value, context) { // anonymous function 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; // there was an error } var responseData = executor.get_responseData(); if (responseData != 'OK') { // add error to validation message var newMessage = (responseData == 'FAIL' ? rule.ErrorMessage : 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; // optimistically assume success }; };
Sys.Mvc.ValidatorRegistry.validators.remoteVal = function(rule) { var url = rule.ValidationParameters.url; var parameterName = rule.ValidationParameters.parameterName; return function(value, context) { // anonymous function 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; // there was an error } var responseData = executor.get_responseData(); if (responseData != 'OK') { // add error to validation message var newMessage = (responseData == 'FAIL' ? rule.ErrorMessage : 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; // optimistically assume success }; };
The first line initializes a client-side validation rule named remoteVal and registers it in the validators collection. (The validators collection is defined in the Scripts\MicrosoftMvcValidation.debug.js file.) The validation rule is defined using an anonymous JavaScript function that returns a function that performs the validation. The function accepts the value to be validated (UID in the example) and a context object that contains information that is specific to the validation that you are performing (such as the default error message and the form context).
The validation code first determines whether there is a value to validate. Validators should return true if they are given an empty value. (To check for a required value, use the RequiredAttributeAdapter class.)
The code then makes sure that validation occurs only in response to the client blur event, which is raised when the field loses focus, such as when the user tabs away from the field. Remote validation should not run in response to key input events (which are raised with each keystroke), because the overhead of making a remote call for each keystroke is high.
The code creates a string that represents the URL to use to invoke the server-based validation and that includes the value to validate. The code then submits the GET request for the URL. In this example, if the user enters ben in the User Name text entry box, the newUrl variable will be set to the following URL:
/Validation/IsUID_Available?candidate=ben
The JavaScript function should read the result and sets the validation error message if the response is anything other than the string "OK".
In the page that contains the UI element to validate (typically an input element), add a reference to the CustomValidation.debug.js file. If your site uses a master page, you typically reference JavaScript files in that file.
Enable client validation. A common way to enable client validation is to call EnableClientValidation in the Site.master page.
The following example from the Site.master page of the downloadable example shows the reference to the CustomValidation.debug.js file, shows supporting JavaScript files, and shows the call to EnableClientValidation. The remote validation script shown in step 2 can be used without modification for most remote client validation implementations. The remote validation script can be used with multiple remote validation calls.
<head runat="server"> <title> <asp:ContentPlaceHolder ID="TitleContent" runat="server" /> </title> <link href="<%= Url.Content("~/Content/Site.css") %>" rel="stylesheet" type="text/css" /> <script src="<%= Url.Content("~/Scripts/MicrosoftAjax.debug.js") %>" type="text/javascript"> </script> <script src="<%= Url.Content("~/Scripts/MicrosoftMvcAjax.debug.js") %>" type="text/javascript"> </script> <script src="<%= Url.Content("~/Scripts/MicrosoftMvcValidation.debug.js") %>" type="text/javascript"> </script> <script src="<%= Url.Content("~/Scripts/CustomValidation.debug.js") %>" type="text/javascript"> </script> <% Html.EnableClientValidation(); %> </head>
<head runat="server"> <title> <asp:ContentPlaceHolder ID="TitleContent" runat="server" /> </title> <link href="<%= Url.Content("~/Content/Site.css") %>" rel="stylesheet" type="text/css" /> <script src="<%= Url.Content("~/Scripts/MicrosoftAjax.debug.js") %>" type="text/javascript"> </script> <script src="<%= Url.Content("~/Scripts/MicrosoftMvcAjax.debug.js") %>" type="text/javascript"> </script> <script src="<%= Url.Content("~/Scripts/MicrosoftMvcValidation.debug.js") %>" type="text/javascript"> </script> <script src="<%= Url.Content("~/Scripts/CustomValidation.debug.js") %>" type="text/javascript"> </script> <% Html.EnableClientValidation()%> </head>
Annotating the Model Class
After you create the custom attribute, you can add the custom annotation to your data model. Because you are adding the attribute to the data model, it is invoked any time the data is accessed through the model.
To annotate a model class with the custom remote validation attribute
Add the custom annotation to your data model.
In the sample download, the following code is used in the UserModel class to annotate a model class with the custom remote validation.
[Required()] [DisplayName("User Name")] [RegularExpression(@"(\S)+", ErrorMessage= "White space is not allowed")] [RemoteUID_(Controller = "Validation", Action = "IsUID_Available", ParameterName = "candidate")] [ScaffoldColumn(false)] public string UserName { get; set; }
The downloadable example uses a simple data model whose values are created, edited, and displayed by using the views templates. For this example, a UserModel class has properties for user name, first name, last name, and city.
Adding a Validation Controller
You must also create a validation controller that contains an action method that performs the custom validation in server code. (You could add this action method to any controller, such as the Home controller, but a best practice is to put validation in its own controller in order to separate concerns and to make testing simpler.)
The action method that you create in the validation controller will test a value and return the string "OK" if the model is valid, and return the string "Fail" if the model fails validation. If "Fail" is returned, the default error message (defined in the ValidationAttribute class or in a class that derives from it) is displayed. If an error string other than ""Fail" is returned, that error string is displayed as the error message.
To add the data-handling controller
In Solution Explorer, right-click the Controllers folder, click Add, and then click Controller.
Give the controller a name such as ValidationController.
At the top of the validation controller class, add the code that performs the server-side validation and that will be called from the client.
The following example shows how to verify that the candidate UID is unique and how to suggest alternatives if it is not unique.
Public Function IsUID_Available(ByVal candidate As String) As String If UserNameHelper.IsAvailable(candidate) Then Return "OK" End If For i As Integer = 1 To 9 Dim altCandidate As String = candidate + i.ToString() If UserNameHelper.IsAvailable(altCandidate) Then Return [String].Format(CultureInfo.InvariantCulture, "{0} is not available. Try {1}.", candidate, altCandidate) End If Next Return [String].Format(CultureInfo.InvariantCulture, "{0} is not available.", candidate) End Function
public string IsUID_Available(string candidate) { if (UserNameHelper.IsAvailable(candidate)) return "OK"; for (int i = 1; i < 10; i++) { string altCandidate = candidate + i.ToString(); if (UserNameHelper.IsAvailable(altCandidate)) return String.Format(CultureInfo.InvariantCulture, "{0} is not available. Try {1}.", candidate, altCandidate); } return String.Format(CultureInfo.InvariantCulture, "{0} is not available.", candidate); }
See Also
Tasks
How to: Validate Model Data Using DataAnnotations Attributes