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
{
}
Very help full
For the following code:
var formatter = new MessageFormatter().AppendPropertyName(Rule.GetDisplayName());
string message = formatter.BuildMessage(RequiredIfClientSideValidator.ErrorMessageSource.GetString());
What does RequiredIfClientSideValidator.ErrorMessageSource look like?
That would be inherited from PropertyValidator. You can checkout fluentvalidation at GitHub . Here’s PropertyValidator: https://github.com/JeremySkinner/FluentValidation/blob/master/src/FluentValidation/Validators/PropertyValidator.cs
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?
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!