ASP.NET MVC [Remote] validation – what about success messages?

January 27th, 2011 / Comments

Since ASP.NET MVC 3 it’s easy to validate an individual form field by firing off a HTTP request to a controller action. This action returns either Json(true) if the value is valid or a Json(“error message”) if it is not valid. If you return an error then jquery.validate.js will add your error message to the validation element for the field being validated. It’s easy to setup on your view model :

	[Required]
[DataType(DataType.EmailAddress)]
[Display(Name = "Email address")]
[EmailAddress]
[Remote("RemoteValidation_UniqueEmailAddress", "Account")]
public string Email { get; set; }
	

When the user makes a change to the Email field the /Account/RemoteValidation_UniqueEmailAddress action is automatically called on the server. The server side action is just as simple to setup :

	public ActionResult RemoteValidation_UniqueEmailAddress( string Email)
{
   object oResult = false;
   if( Validation.IsEmailAddress( Email) == false)
   {
      return Json( "Please enter a valid email address", JsonRequestBehavior.AllowGet);
   }
   else if( m_oMembershipService.IsThisEmailAlreadyTaken( Email) == true)
   {
      return Json( "This email address is already in use", JsonRequestBehavior.AllowGet);
   }
   else
   {
      return Json( true, JsonRequestBehavior.AllowGet);
   }
}
	

A remote validation error is displayed in exactly the same way as a view would render it. This is because the javascript latches onto the Html.ValidationMessageFor element you setup in your view :


This is ok isn’t it? But…

I want the Email Address label to be bold and red too

In the interests of best practice web form techniques I want the email address label to be bold and red, as well as the error message. To do this I had to hack jquery.validate.js so that it calls a local javascript function with the same name as the server function (with “_response” appended to it) :

	remote: function(value, element, param) {
    if ( this.optional(element) )
        return "dependency-mismatch";
 
    var previous = this.previousValue(element);
    if (!this.settings.messages[element.name] )
        this.settings.messages[element.name] = {};
 
    previous.originalMessage = this.settings.messages[element.name].remote;
    this.settings.messages[element.name].remote = previous.message;
 
    param = typeof param == "string" && {url:param} || param;
 
    if ( previous.old !== value ) {
        previous.old = value;
        var validator = this;
        this.startRequest(element);
        var data = {};
        data[element.name] = value;
 
        $.ajax($.extend(true, {
            url: param,
            mode: "abort",
            port: "validate" + element.name,
            dataType: "json",
            data: data,
            success: function(response) {
 
                //START OF HACK
                var aParts = param.url.split(/\//);//eg; param.url = "/Account/RemoteValidation_UniqueEmailAddress"    ELMS
                oCustomResultFunction = aParts[aParts.length - 1] + "_response";//ELMS
                //END OF HACK
 
                validator.settings.messages[element.name].remote = previous.originalMessage;
                var valid = response === true;
 
                if ( valid ) {
                    var submitted = validator.formSubmitted;
                    validator.prepareElement(element);
                    validator.formSubmitted = submitted;
                    validator.successList.push(element);
 
        //START OF HACK
        var bShowErrors = true;
        if( typeof(window[oCustomResultFunction]) == "function")
        {
            bShowErrors = window[oCustomResultFunction]( true, null, validator);
        }
        //END OF HACK
 
        if( bShowErrors)//HACK
        {
            validator.showErrors();
        }
 
        }
        else
        {
            var errors = {};
            var message = (previous.message = response || validator.defaultMessage( element, "remote" ));
            errors[element.name] = $.isFunction(message) ? message(value) : message;
 
            //START OF HACK
            var bShowErrors = true;
            if( typeof(window[oCustomResultFunction]) == "function")
            {
                bShowErrors = window[oCustomResultFunction]( false, errors, validator);
            }
            //END OF HACK
 
            if( bShowErrors)//HACK
            {
                validator.showErrors(errors);
            }
        }
 
        previous.valid = valid;
        validator.stopRequest(element, valid);
    }
}, param));
        return "pending";
    } else if( this.pending[element.name] ) {
        return "pending";
    }
    return previous.valid;
},
	

And now a client side javacsript function called RemoteValidation_UniqueEmailAddress_response will be called with the result of the remote validation. In this example I know that the function only targets the Email field but it’s wise to check the errors collection that comes back in case other model errors were added in the server function (see the outcommented code below). The aErrors collection is keyed on the client side ID of the invalid field/s :

	function RemoteValidation_UniqueEmailAddress_response( bIsValid, aErrors, oValidator)
{
    if( bIsValid == false)
    {
        /* This bit is optional
        for( var sId in aErrors)
        {
            $("label[for=" + sId + "]").addClass( "error").css( "font-weight", "bold");
        }*/
 
        $("label[for=Email]").addClass( "error").css( "font-weight", "bold");
    }
    else
    {
        $("label[for=Email]").removeClass( "error").css( "font-weight", "normal");
    }
 
    return true;
}
	

The first argument to the function is whether there is an error or not. The second parameter is the errors collection. Now when I enter an invalid email address my form looks like this

That’s better but something is still bothering me

Success messages would be nice

As well as catering for validation errors you now get the chance to provide success feedback as well which can be just as important as error feedback. For example if the user is registering on a site and choosing a username the validation could check that the username is available. If it is then you can present a jolly green tick to the user. This completely removes ambiguiuty, especially if the previous username they entered was already taken and they saw a validation error

One last bit of code :

	var g_bEmailHasBeenInErrorState = false;
function RemoteValidation_UniqueEmailAddress_response( bIsValid, aErrors, oValidator)
{
    if( bIsValid == false)
    {
        $("label[for=Email]").addClass( "error").css( "font-weight", "bold");
        g_bEmailHasBeenInErrorState = true;
    }
    else
    {
        $("label[for=Email]").removeClass( "error").css( "font-weight", "normal");
 
        if( g_bEmailHasBeenInErrorState)
        {
            g_bEmailHasBeenInErrorState = false;
 
            $("#Email").after( "<img style='float:left;' src='<%=Url.Content( "~/Content/img/tick.gif")%>' alt='This is a valid email for this web site' />");
        }
    }
 
    return true;
}
	

Hang on a minute. Why isn’t this functionality in jquery.validate.js anyway? Go to your room!

I didn’t refer once to AJAX because :

  • XML is moribund everywhere but the enterprise.
  • IE had javascript HTTP requests before the web existed.
  • Prior to that you could just screen scrape a hidden iframe for crying out loud!

ASP.NET MVC , jQuery , UI