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.

Remote UID Validation

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

  1. 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.

  2. Create a folder in the project that can contain the validation classes. For example, create a folder named Validation.

  3. 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.

  4. Create a validation adapter class that has the following characteristics:

  5. 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.

  6. 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

  1. In the Scripts folder, create a new JScript (.js) file.

  2. 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".

  3. 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.

  4. 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

  1. In Solution Explorer, right-click the Controllers folder, click Add, and then click Controller.

  2. Give the controller a name such as ValidationController.

  3. 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

Other Resources

Using the New MVC 2 Templated Helpers