Integrating FluentValidation with MvcContrib’s Fluent Html Helpers
December 13, 2008 – 14:00
A recent addition to the MvcContrib project are a set of HTML helpers that use a fluent interface written by Tim Scott.
One of the great things about these helpers is that you can change how the HTML is rendered by writing custom IMemberBehavior objects. For example, the helpers come with a set of attributes that you can use to decorate your model:
public class Customer { [MaxLength(50)] public string Name { get; set; } }
Then, when you render your view, the HTML helper will automatically detect the presence of this attribute and render a “maxlength” attribute accordingly. For example, this textbox helper…
<%= this.TextBox(m => m.Customer.Name) %>
…would render this html:
<input type="text" maxlength="50" id="Customer_Name" name="Customer.Name" />I wanted to take this a step further and integrate it with my validation library, so the HTML helpers will automatically pick up maxlength/required attributes from my validator classes.
Step 1: Custom Validator Class
The first stage was to create a metadata class to hold information about a validator.
public class PropertyModel { public int MaxLength { get; set; } public bool Required { get; set; } public PropertyInfo Property { get; private set; } public PropertyModel(PropertyInfo property) { Property = property; } }
Next, I introduced a new interface, IPropertyDescriptor, that all my validator classes will implement. This interface defines the signature for a method that creates PropertyModel instances based on a PropertyInfo:
public interface IPropertyDescriptor { PropertyModel GetPropertyModel(PropertyInfo property); }
Next, I added a custom base-class for all my validators which implements the above interface:
public abstract class Validator<T> : AbstractValidator<T>, IPropertyDescriptor { public PropertyModel GetPropertyModel(PropertyInfo property) { var model = new PropertyModel(property); var validators = this.OfType<IPropertyValidatorContainer<T>>().Where(x => x.Property == property).Select(x => x.Validator); foreach (var validator in validators) { if (HandleLengthValidator(validator, model)) continue; if (HandleRequiredValidator(validator, model)) continue; } return model; } private bool HandleLengthValidator(IPropertyValidator<T> validator, PropertyModel model) { var length = validator as ILengthValidator; if(length != null) { model.MaxLength = length.Max; return true; } return false; } private bool HandleRequiredValidator(IPropertyValidator<T> validator, PropertyModel model) { var required = validator as INotNullValidator; if(required != null) { model.Required = true; return true; } return false; } }
Essentially, the GetPropertyModel method iterates through all of the property validators to see if any required or max-length validators have been defined. If so, it records them in the PropertyModel object.
I can now define my validators as usual, but they now inherit from my new Validator<T> class, rather than AbstractValidator<T>.
public class CustomerValidator : Validator<Customer> { public CustomerValidator() { RuleFor(customer => customer.Name).Length(0, 50); } }
Step 2: Custom Validator Factory
The next stage is to impelement a custom validator factory that can be used to instantiate validators. For this example, I’ll be using the Windsor IoC container.
Firstly, in my application startup routine I need to register all of my validators with Windsor:
var container = new WindsorContainer(); container.Register(AllTypes.Of(typeof(IValidator<>)).FromAssembly(typeof(CustomerValidator).Assembly).WithService.Base());
Next, I create a validator factory that uses Windsor (or more accurently, Microkernel) to create the validator instances.
public class ValidatorFactory : IValidatorFactory { private readonly IKernel container; public ValidatorFactory(IKernel container) { this.container = container; } public IValidator<T> GetValidator<T>() { return container.TryResolve<IValidator<T>>(); } public IValidator GetValidator(Type type) { var genericType = typeof(IValidator<>).MakeGenericType(type); return (IValidator)container.TryResolve(genericType); } }
Note that TryResolve is an extension method on IKernel that will attept to resolve a component and return null if it fails (by default, Windsor/Microkernel will throw an exception).
This validator factory can now be registered with the container:
container.Register(Component.For<IValidatorFactory>().ImplementedBy<ValidatorFactory>());
Step 3: The PropertyModelFactory
The next stage is to create a class that can be used to locate the correct validator from a PropertyInfo object (by using the validator factory) and return a corresponding PropertyModel.
public class PropertyModelFactory : IPropertyDescriptor { private IValidatorFactory validatorFactory; public PropertyModelFactory(IValidatorFactory validatorFactory) { this.validatorFactory = validatorFactory; } public PropertyModel GetPropertyModel(PropertyInfo property) { var descriptor = validatorFactory.GetValidator(property.ReflectedType) as IPropertyDescriptor; if(descriptor != null) { return descriptor.GetPropertyModel(property); } return null; } }
Again, this needs to be registered with the container:
container.Register(Component.For<IPropertyDescriptor>().ImplementedBy<PropertyModelFactory>());
Step 4: Custom IMemberBehavior
Next, we need to create a custom IMemberBehavior that will make use of the PropertyModelFactory:
public class ValidatorMemberBehaviour : IMemberBehavior { private readonly IPropertyDescriptor descriptor; public ValidatorMemberBehaviour(IPropertyDescriptor descriptor) { this.descriptor = descriptor; } public void Execute(IMemberElement element) { if (element.ForMember == null) return; var property = element.ForMember.Member as PropertyInfo; if(property == null) return; var model = descriptor.GetPropertyModel(property); if (model == null) return; var maxLengthMethod = element.GetType().GetMethod("MaxLength"); if(maxLengthMethod != null) { element.Builder.MergeAttribute("maxlength", model.MaxLength.ToString()); } if(model.Required) { element.Builder.AddCssClass("required"); } } }
MemberBehaviors are invoked when certain HTML helper objects are created, so this behaviour now needs to be registered with our ViewPage:
public class MyViewPage<T> : ModelViewPage<T> where T : class { public MyViewPage() : base(new ValidatorMemberBehaviour(ServiceLocator.Resolve<IPropertyDescriptor>())) { } }
Note that the ServicLocator static class is just a wrapper around Windsor.
So now the HTML Helpers will use my validator definitions to generate required/maxlength attributes. I think the extensibility of these helpers is incredibly powerful and I am planning on using them going forward in my next project.
6 Responses
Very cool! I had envisioned using IMemberBehavior only with attributes, but because it has a handle on the member, it supports this usage nicely!
One thought. It might be better to use element.SetAttr method instead of element.Builder. Two reasons. First, Law of Demeter. Second, you are assuming some knowledge of inner workings of the class. I wonder indeed if it’s unwise that we expose Builder. I recall that I did it to facilitate testing some edge cases. Perhaps a not a good idea?
But this is a small point. Nice work!
Thanks for the tip.
Personally, I’d prefer to keep the tagbuilder exposed. I think its a useful extensibility point that gives you the option of modifying the HTML before it is rendered.
For example, I have another custom MemberBehavior that extracts the ‘name’ attribute from the TagBuilder and lowercases the first character (which is a convention in the app I’m currently working on).
Another approach might be to extend the API to do whatever you might want to do with TagBuilder. IElement.GetAttr(string key) might provide what you need while keeping TagBuilder (someone else’s class, which is still beta) encapsulated.
That’s a better idea. I’ll try and implement this at some point this evening (unless you beat me to it
)
Nice solution. I see you’re working with the IPropertyValidators, though. Wouldn’t it be more useful to get the PropertyRule for each of the validators so you have access to the message?
Hi Nick,
Yes that’s probably a better idea. This was just a proof of concept to show that it was possible.