Partial forms validation in ASP.NET MVC 2 (something like WebForms ValidationGroup)

In one of my recent ASP.NET MVC 2 projects I had to make a form divided into few tabs. Each tab had to be validated before user can move to next one. Unfortunately ASP.NET MVC 2 built in validation doesn't provide mechanism for this. Well we are developers, so if there is no built in mechanism, we should create our own. First lets create a ValidationAttribute, which will be used to add information about validation group:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class ValidationGroupAttribute : ValidationAttribute
{
  #region Properties
  /// <summary>
  /// Name of the validation group
  /// </summary>
  public string GroupName { get; set; }
  #endregion

  #region Constructor
  public ValidationGroupAttribute(string groupName)
  {
    GroupName = groupName;
  }
  #endregion

  #region Methods
  public override bool IsValid(object value)
  {
    //No validation logic, always return true
    return true;
  }
  #endregion
}

We will also need a DataAnnotationsModelValidator for this attribute:
public class ValidationGroupValidator : DataAnnotationsModelValidator<ValidationGroupAttribute>
{
  #region Fields
  string _groupName;
  #endregion

  #region Constructor
  public ValidationGroupValidator(ModelMetadata metadata, ControllerContext context, ValidationGroupAttribute attribute)
: base(metadata, context, attribute) 
  {
    _groupName = attribute.GroupName;
  }
  #endregion

  #region Methods
  public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
  {
    //Add informations about validation group to metadata
    var validatioRule = new ModelClientValidationRule
    {
      ErrorMessage = String.Empty,
      ValidationType = "validationGroup"
    };
    validatioRule.ValidationParameters.Add("groupName", _groupName);

    return new[] { validatioRule };
  }
  #endregion
}

Don't forget to register above classes in your Global.asax:
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(ValidationGroupAttribute), typeof(ValidationGroupValidator));

So this is all we need on server side (please remember, that we don't want to achieve any server side logic, only partial validation on client side before posting the whole form). To make it work on client side, first we need to modify Sys.Mvc.FormContext._parseJsonOptions in MicrosoftMvcValidation.debug.js (or corresponding part in MicrosoftMvcValidation.js):
Sys.Mvc.FormContext._parseJsonOptions = function Sys_Mvc_FormContext$_parseJsonOptions(options) {
  var formElement = $get(options.FormId);
  var validationSummaryElement = (!Sys.Mvc._validationUtil.stringIsNullOrEmpty(options.ValidationSummaryId)) ? $get(options.ValidationSummaryId) : null;
  var formContext = new Sys.Mvc.FormContext(formElement, validationSummaryElement);
  formContext.enableDynamicValidation();
  formContext.replaceValidationSummary = options.ReplaceValidationSummary;
  for (var i = 0; i < options.Fields.length; i++) {
    var field = options.Fields[i];
    var fieldElements = Sys.Mvc.FormContext._getFormElementsWithName(formElement, field.FieldName);
    var validationMessageElement = (!Sys.Mvc._validationUtil.stringIsNullOrEmpty(field.ValidationMessageId)) ? $get(field.ValidationMessageId) : null;
    var fieldContext = new Sys.Mvc.FieldContext(formContext);
    Array.addRange(fieldContext.elements, fieldElements);
    fieldContext.validationMessageElement = validationMessageElement;
    fieldContext.replaceValidationMessageContents = field.ReplaceValidationMessageContents;
    for (var j = 0; j < field.ValidationRules.length; j++) {
      var rule = field.ValidationRules[j];
      //Here goes our small modification
      if (rule.ValidationType == 'validationGroup') {
        fieldContext.validationGroup = rule.ValidationParameters['groupName'];
      }
      else {
        var validator = Sys.Mvc.ValidatorRegistry.getValidator(rule);
        if (validator) {
          var validation = Sys.Mvc.$create_Validation();
          validation.fieldErrorMessage = rule.ErrorMessage;
          validation.validator = validator;
          Array.add(fieldContext.validations, validation);
        }
      }
    }
    fieldContext.enableDynamicValidation();
    Array.add(formContext.fields, fieldContext);
  }
  var registeredValidatorCallbacks = formElement.validationCallbacks;
  if (!registeredValidatorCallbacks) {
    registeredValidatorCallbacks = [];
    formElement.validationCallbacks = registeredValidatorCallbacks;
  }
  registeredValidatorCallbacks.push(Function.createDelegate(null, function () {
    return Sys.Mvc._validationUtil.arrayIsNullOrEmpty(formContext.validate('submit'));
  }));
  return formContext;
}

Now we can write ourselves function, which will perform partial validation based on group name:
Sys.Mvc.FormContext.validateGroup = function Sys_Mvc_FormContext$validateGroup(formId, groupName) {
  //Get form element
  var formElement = $get(formId);
  //Get form context
  var formContext = Sys.Mvc.FormContext.getValidationForForm(formElement);
  //Get form fields
  var fields = formContext.fields;
  //Array for errors
  var errors = [];
  //For each field
  for (var i = 0; i < fields.length; i++) {
    var field = fields[i];
    //If field has validation group and its name matches the one we are looking for
    if (field.validationGroup && field.validationGroup == groupName) {
      //Validate field
      var fieldErrors = field.validate('submit');
      if (fieldErrors) {
        Array.addRange(errors, fieldErrors);
      }
    }
  }
  //Return true it there are no errors, otherwise false
  return (!errors || !errors.length);
}

And our job is done. Now we can call Sys.Mvc.FormContext.validateGroup('formId', 'validationGroup'); whenever we want to perform partial validation. I have created a sample application which make use of those modification, you can download it from my repository. If there is a need for same modification for jQuery validation, let me know and I will look into it.

10 comments:

Anonymous said...

Genial dispatch and this enter helped me alot in my college assignement. Say thank you you for your information.

George said...

I had same problem and I was trying to use the code in link you provided https://tpeczek.svn.codeplex.com/svn/trunk/MVC/PartFormValidationExample/. But there is no Lib.Web.Mvc project in the code here.

Tomasz Pęczek said...

You can find it here: https://tpeczek.svn.codeplex.com/svn/trunk/MVC/Lib.Web.Mvc/

Rodrigo said...

Hi Tomas,

I think your article is great! but I need to implement it with asp.net mvc 3's unobtrusive validation,

Do you know where do I have to change the jquery.validate.unobtrusive.js file?

best regards

Tomasz Pęczek said...

Hi Rodrigo,

I will look into this and let you know.

Greetings

Tomasz Pęczek said...

Hi Rodrigo,

The ASP.NET MVC 3 jQuery client-side unobtrusive validation works a lot different than ASP.NET MVC 2 Microsoft client-side validation (for which the original solution was made). I'm not sure if this approach will work at all. I will keep on working on this, but as I have very limited time lately, this may take a while.

Greetings

Eduardo said...

Hi Tomasz. Did you get it with MVC 3?

Tomasz Pęczek said...

Unfortunately I haven't - because of a new project my resources are too limited recently to take proper care of this subject. I will get back to this as soon as possible.

Eduardo said...

Tomasz, I got it!
The solution is here:
http://softwaredevelopmentsolutions.blogspot.com/2011/06/aspnet-mvc-3-partial-form-validation-on.html

Martin Jarvis said...

Hi,

There's unbtrusive MVC3 client and server side implementation of validation groups in the 'MVC Validation Groups' CodePlex project (http://bit.ly/zRmxwD).

I wrote it specifically to handle tabbed UI's where validation summaries need to be split betweek multiple screens when there's a single 'submit' button.

A neat side affect is that it allows different validation to be performed based on different buttons.

Martin