FluentValidation Client-Side Custom Validation with MVC

FluentValidation is “A small validation library for .NET that uses a fluent interface and lambda expressions for building validation rules for your business objects.” It is quite awesome. However, the provided client-side validation is limited:  “any rules defined using a condition (with When/Unless), custom validators, or calls to Must will not run on the client side.” Below is some my first take on using FluentValidation on both server and client-side.

First, you need to install the nuget package. In our case, it is FluentValidation.MVC4.

We needed to make a certain property required, but only when some other property is true. To achieve this on client-side (server-side is trivial), we added these:

	public class RequiredIfClientSideValidator : PropertyValidator
	{
		public string DependentProperty { get; set; }
		public object TargetValue { get; set; }

		public RequiredIfClientSideValidator(string errorMessage, string dependentProperty, object targetValue)
			: base(errorMessage)
		{
			this.DependentProperty = dependentProperty;
			this.TargetValue = targetValue;
		}

		protected override bool IsValid(PropertyValidatorContext context)
		{
			//This is not a server side validation rule. So, should not effect at the server side.  
			return true;
		}
	}

And this

	public class RequiredIfClientSideFluentPropertyValidator : FluentValidationPropertyValidator
	{
		private RequiredIfClientSideValidator RequiredIfClientSideValidator
		{
			get { return (RequiredIfClientSideValidator)Validator; }
		}

		public RequiredIfClientSideFluentPropertyValidator(ModelMetadata metadata,
													   ControllerContext controllerContext,
													   PropertyRule propertyDescription,
													   IPropertyValidator validator)
			: base(metadata, controllerContext, propertyDescription, validator)
		{
			ShouldValidate = false;
		}

		public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
		{
			if (!ShouldGenerateClientSideRules()) yield break;

			var formatter = new MessageFormatter().AppendPropertyName(Rule.GetDisplayName());
			string message = formatter.BuildMessage(RequiredIfClientSideValidator.ErrorMessageSource.GetString());

			var rule = new ModelClientValidationRule()
			{
				ValidationType = "requiredif",
				ErrorMessage = message
			};

			string depProp = BuildDependentPropertyId(Metadata, ControllerContext as ViewContext);
			// find the value on the control we depend on;
			// if it's a bool, format it javascript style 
			// (the default is True or False!)
			string targetValue = (RequiredIfClientSideValidator.TargetValue ?? "").ToString();
			if (RequiredIfClientSideValidator.TargetValue.GetType() == typeof(bool))
				targetValue = targetValue.ToLower();

			rule.ValidationParameters.Add("dependentproperty", depProp);
			rule.ValidationParameters.Add("targetvalue", targetValue);

			yield return rule;
		}

		private string BuildDependentPropertyId(ModelMetadata metadata, ViewContext viewContext)
		{
			// build the ID of the property
			string depProp = viewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(RequiredIfClientSideValidator.DependentProperty);
			// unfortunately this will have the name of the current field appended to the beginning,
			// because the TemplateInfo's context has had this fieldname appended to it. Instead, we
			// want to get the context as though it was one level higher (i.e. outside the current property,
			// which is the containing object (our Person), and hence the same level as the dependent property.
			var thisField = metadata.PropertyName + "_";
			if (depProp.StartsWith(thisField))
				// strip it off again
				depProp = depProp.Substring(thisField.Length);
			return depProp;
		}
	}

Additionally, we introduced a validator for simple client-side validation (ClientSideValidator) where you just need to pass an error message to your JavaScript validation.

public class ClientSideValidator : PropertyValidator
	{
		public string JavascriptValidationAdapterName { get; set; }

		public ClientSideValidator(string errorMessage, string javascriptValidationFunctionName)
			: base(errorMessage)
		{
			JavascriptValidationAdapterName = javascriptValidationFunctionName;
		}

		protected override bool IsValid(PropertyValidatorContext context)
		{
			//This is not a server side validation rule. So, should not effect at the server side.  
			return true;
		}
	}

And an accompanying class:

	
	public class ClientSideFluentValidator : FluentValidationPropertyValidator
	{
		private ClientSideValidator ClientSideValidator
		{
			get { return (ClientSideValidator)Validator; }
		}

		public ClientSideFluentValidator(ModelMetadata metadata,
			ControllerContext controllerContext,
			PropertyRule propertyDescription,
			IPropertyValidator validator)
			: base(metadata, controllerContext, propertyDescription, validator)
		{
			ShouldValidate = false;
		}

		public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
		{
			if (!ShouldGenerateClientSideRules()) yield break;

			var formatter = new MessageFormatter().AppendPropertyName(Rule.GetDisplayName());
			string message = formatter.BuildMessage(ClientSideValidator.ErrorMessageSource.GetString());

			var rule = new ModelClientValidationRule()
			{
				ValidationType = ClientSideValidator.JavascriptValidationAdapterName,
				ErrorMessage = message
			};
			
			yield return rule;
		}
	}

Register your custom validators in Global.asax.cs:

FluentValidationModelValidationFactory requiredIfClientSideValidationFactory =
				(metadata, context, rule, validator) =>
					new RequiredIfClientSideFluentPropertyValidator(metadata, context, rule, validator);
			
			FluentValidationModelValidationFactory clientSideValidationFactory =
				(metadata, context, rule, validator) =>
					new ClientSideFluentValidator(metadata, context, rule, validator);
			
			FluentValidationModelValidatorProvider.Configure(provider =>
			{
				provider.Add(typeof(RequiredIfClientSideValidator), requiredIfClientSideValidationFactory);
				provider.Add(typeof(ClientSideValidator), clientSideValidationFactory);
			});

Don’t forget to add the JavaScript part:
For requiredif validation:

$.validator.addMethod('requiredif',
    function (value, element, parameters) {
    	var id = '#' + parameters['dependentproperty'];

    	// get the target value (as a string, 
    	// as that's what actual value will be)
    	var targetvalue = parameters['targetvalue'];
    	targetvalue =
          (targetvalue == null ? '' : targetvalue).toString();

    	// get the actual value of the target control
    	// note - this probably needs to cater for more 
    	// control types, e.g. radios
    	var control = $(id);
    	var controltype = control.attr('type');
    	var actualvalue =
            controltype === 'checkbox' ?
            control.attr('checked').toString() :
            control.val();

    	// if the condition is true, reuse the existing 
    	// required field validator functionality
    	if (targetvalue.toLowerCase() === actualvalue.toLowerCase())
    		return $.validator.methods.required.call(
              this, value, element, parameters);

    	return true;
    }
);

$.validator.unobtrusive.adapters.add(
    'requiredif',
    ['dependentproperty', 'targetvalue'],
    function (options) {
    	options.rules['requiredif'] = {
    		dependentproperty: options.params['dependentproperty'],
    		targetvalue: options.params['targetvalue']
    	};
    	options.messages['requiredif'] = options.message;
    });

And for propertycvalidator:

$.validator.addMethod('propertycvalidator', function (value, element, parameters) {
			//put your client-side validation logic here
			return true;
		});

		$.validator.unobtrusive.adapters.addBool('propertycvalidator');

And, now we are ready to create our model validator:

		public class MyValidator()
		{
			//Make SomeProperty required. It will be validated on server and client side.
			RuleFor(m => m.SomeProperty)
				.NotEmpty().WithMessage("Some Property is required");
			//SomeOtherProperty will be validated on server side - it is required when PropertyB is true.
			//Client side validation is done by using RequiredIfClientSideValidator and passing dependent property 
			//name to unubtrusive JavaScript validation
			RuleFor(m => m.SomeOtherProperty)
				.NotNull()
				.WithMessage("Some other property is required")
				.When(model => model.PropertyB)
				.SetValidator(new RequiredIfClientSideValidator(
					"Some other property is required",
					"PropertyB",
					true));

			//this will get validated on client side only
			RuleFor(m => m.PropertyC)				
				.SetValidator(
					new ClientSideValidator(
						"Error message",
						"propertycvalidator")); // must be lower case
		}

The last part is adding the attribute to your model:
[Validator(typeof(MyValidator))]
public class YourModel
{
}

5 responses on “FluentValidation Client-Side Custom Validation with MVC

  1. Brian

    For the following code:


    var formatter = new MessageFormatter().AppendPropertyName(Rule.GetDisplayName());
    string message = formatter.BuildMessage(RequiredIfClientSideValidator.ErrorMessageSource.GetString());

    What does RequiredIfClientSideValidator.ErrorMessageSource look like?

  2. Jonathan

    Hi Adam, I have implemented your code as is and get the following error when I try to use this on any MVC5 view:

    “Additional information: Validation type names in unobtrusive client validation rules must be unique. The following validation type was seen more than once: required”

    Where should I look to fix this?

    1. Adam Z Post author

      Jonathan – my guess is that you have a [Required] attribute on one of your models. You would need to remove it and handle it with fluent validation code. Hope it helps!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.