Wednesday 28 March 2012

Extending the MVC3 RemoteAttribute to validate server-side when JavaScript is disabled

In ASP.NET MVC3 the RemoteAttribute class works seamlessly with unobtrusive JavaScript to asynchronously execute server side validation via AJAX.

However, one of the problems with this approach arises when JavaScript is disabled on the user's browser.  A thorough approach to validation will not rely on JavaScript being enabled, but the traditional DRY approach  suggested by many is simply to encapsulate your validation logic in a method and call this from both your RemoteAttribute controller action and also on your page submission code as a backup, calling ModelState.AddModelError() if the validation fails, passing the model back to the view to signal that validation failed.  Sadly, this renders the separation of concerns within MVC validation redundant as the controller method is now responsible for performing validation as well as its intended business logic.

A simple approach to fixing this error is to extend the existing RemoteAttribute class, overriding the IsValid method to call your remote controller method from the server.  The code sample below works well for controller methods that return a JsonResult containing a boolean value.

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class RemoteWithServerSideAttribute : RemoteAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        string controllerName = this.RouteData["controller"].ToString();
        string actionName = this.RouteData["action"].ToString();
        string[] additionalFields = this.AdditionalFields.Split(',');

        List<object> propValues = new List<object>();
        propValues.Add(value);
        foreach (string additionalField in additionalFields)
        {
            PropertyInfo prop = validationContext.ObjectType.GetProperty(additionalField);
            if (prop != null)
            {
                object propValue = prop.GetValue(validationContext.ObjectInstance, null);
                propValues.Add(propValue); 
            }
        }

        Type controllerType = Assembly.GetExecutingAssembly().GetTypes().FirstOrDefault(t => t.Name.ToLower() == (controllerName + "Controller").ToLower());
        if (controllerType != null)
        {
            object instance = Activator.CreateInstance(controllerType);

            MethodInfo method = controllerType.GetMethod(actionName);

            if (method != null)
            {
                ActionResult response = (ActionResult)method.Invoke(instance, propValues.ToArray());

                ValidationResult output = null;

                if (response is JsonResult)
                {
                    bool isAvailable = false;
                    JsonResult json = (JsonResult)response;
                    string jsonData = json.Data.ToString();

                    bool.TryParse(jsonData, out isAvailable);

                    if (!isAvailable)
                    {
                        return new ValidationResult(this.FormatErrorMessage(validationContext.DisplayName));
                    }
                    else
                    {
                        return null;
                    }
                }
            }
        }

        return null;
    }    

    public RemoteWithServerSideAttribute(string routeName)
        : base()
    {
    }

    public RemoteWithServerSideAttribute(string action, string controller)
        : base(action, controller)
    {
    }

    public RemoteWithServerSideAttribute(string action, string controller, string areaName)
        : base(action, controller, areaName)
    {
    }

}

No comments:

Post a Comment