Integrating Regula with ASP.NET MVC and DataAnnotations

Previously I discovered Regula, an annotation-based validation framework used for client-side validation of form elements. I wondered if it was possible to automatically wire up client-side validation using DataAnnotation’s ValidationAttributes on the server side, very similar to how xVal might handle it.

This is by no means usable, its really an experiment to see what is possible.

Resolving ValidationAttributes us Regula constraints
Right, first things first, we need some way of mapping between a ValidationAttribute, and its representation as a Regula constraint. To do this, we can create an interface contract which simply resolves an attribute as a constraint:

/// /// Defines the required contract for implementing a Regula attribute resolver. ///
public interface IAttributeResolver
{
#region Methods
/// /// Resolves the specified attribute as a Regula constraint. ///
/// The attribute to resolve.
/// The resolved constraint.
string Resolve(ValidationAttribute attribute);
#endregion
}
Next, we need to create a type resposible for calling these resolvers, so we’ve created the RegulaAdapter. This type will have a collection of registered resolvers that are mapped against attribute types:

/// /// Provides an adapter for translating validation attributes to Regula constraints. ///
/// The model type.
public class RegulaAdapter
{
#region Fields
private static readonly IDictionary Resolvers;
#endregion

#region Constructor /// <summary> /// Statically initialises the <see cref="RegulaAdapter{TModel}"/> type. /// </summary> static RegulaAdapter() { Resolvers = new Dictionary<Type, IAttributeResolver>(); RegisterDefaultResolvers(); } #endregion #region Methods /// <summary> /// Registers an attribute resolver for the specified attribute. /// </summary> /// <typeparam name="TAttribute">The type of attribute.</typeparam> /// <typeparam name="TResolver">The type of resolver.</typeparam> public static void RegisterResolver<TAttribute, TResolver>() where TAttribute : ValidationAttribute where TResolver: IAttributeResolver, new() { RegisterResolver<TAttribute, TResolver>(new TResolver()); } /// <summary> /// Registers an attribute resolver for the specified attribute. /// </summary> /// <typeparam name="TAttribute">The type of attribute.</typeparam> /// <typeparam name="TResolver">The type of resolver.</typeparam> /// <param name="resolver">The instance of resolver to register.</param> public static void RegisterResolver<TAttribute, TResolver>(TResolver resolver) where TAttribute : ValidationAttribute where TResolver : IAttributeResolver { var type = typeof(TAttribute); if (Resolvers.ContainsKey(type)) Resolvers[type] = resolver; else Resolvers.Add(type, resolver); } /// <summary> /// Registers any default resolvers. /// </summary> private static void RegisterDefaultResolvers() { RegisterResolver<RequiredAttribute, SimpleAttributeResolver>(new SimpleAttributeResolver("@Required")); RegisterResolver<EmailAttribute, SimpleAttributeResolver>(new SimpleAttributeResolver("@Email")); RegisterResolver<RangeAttribute, RangeAttributeResolver>(); } #endregion
Code language: PHP (php)

}
We can provide some default resolvers, based on known validation attributes, e.g.:

/// /// Defines a simple attribute resolver. ///
public class SimpleAttributeResolver : IAttributeResolver
{
#region Fields
private readonly string Constraint;
#endregion

#region Constructor /// <summary> /// Initialises a new instance of <see cref="SimpleAttributeResolver"/> /// </summary> /// <param name="constraint">The constraint to return.</param> public SimpleAttributeResolver(string constraint) { if (string.IsNullOrEmpty(constraint)) throw new ArgumentException("'constraint' cannot be null or empty."); Constraint = constraint; } #endregion #region Methods /// <summary> /// Resolves the specified attribute as a Regula constraint. /// </summary> /// <param name="attribute">The attribute to resolve.</param> /// <returns>The resolved constraint.</returns> public string Resolve(ValidationAttribute attribute) { return Constraint; } #endregion
Code language: PHP (php)

}

/// /// Defines a simple attribute resolver. ///
public class RangeAttributeResolver : IAttributeResolver
{
#region Methods
/// /// Resolves the specified attribute as a Regula constraint. ///
/// The attribute to resolve.
/// The resolved constraint.
public string Resolve(ValidationAttribute attribute)
{
var attr = attribute as RangeAttribute;
if (attr == null)
throw new InvalidOperationException(“RangeAttributeResolver requires an instance of RangeAttribute”);

return string.Format("@Range(min={0}, max={1})", attr.Minimum, attr.Maximum); } #endregion
Code language: PHP (php)

}
We can also create a custom attribute and map that to a resolver:

/// /// Validates a field against an email regular expression. ///
public class EmailAttribute : RegularExpressionAttribute
{
#region Fields
private const string RegexPattern = @”b[A-Z0-9._%-][email protected][A-Z0-9.-]+.[A-Z]{2,4}b”;
#endregion

#region Constructor /// <summary> /// Initialises a new instance of <see cref="EmailAttribute"/> /// </summary> public EmailAttribute() : base(RegexPattern) { } #endregion
Code language: PHP (php)

}
Wiring up client-side validation
To handle our client-side validation, we need some way of appending the Regula html attributes to standard controls. I haven’t found an easy way of doing this without writing a series of extension methods such as RegulaTextBoxFor(…) etc. This is only a first version, so I don’t really care at the moment…

Let’s finish off our implementation of our RegulaAdapter:

/// /// Provides an adapter for translating validation attributes to Regula constraints. ///
/// The model type.
public class RegulaAdapter
{
#region Fields
private static readonly IDictionary Resolvers;
#endregion

#region Constructor /// <summary> /// Statically initialises the <see cref="RegulaAdapter{TModel}"/> type. /// </summary> static RegulaAdapter() { Resolvers = new Dictionary<Type, IAttributeResolver>(); RegisterDefaultResolvers(); } /// <summary> /// Initialises a new instance of <see cref="RegulaAdapter{TModel}"/> /// </summary> public RegulaAdapter() { ModelType = typeof(TModel); } #endregion #region Properties /// <summary> /// Gets the model type. /// </summary> public Type ModelType { get; private set; } #endregion #region Methods /// <summary> /// Registers an attribute resolver for the specified attribute. /// </summary> /// <typeparam name="TAttribute">The type of attribute.</typeparam> /// <typeparam name="TResolver">The type of resolver.</typeparam> public static void RegisterResolver<TAttribute, TResolver>() where TAttribute : ValidationAttribute where TResolver: IAttributeResolver, new() { RegisterResolver<TAttribute, TResolver>(new TResolver()); } /// <summary> /// Registers an attribute resolver for the specified attribute. /// </summary> /// <typeparam name="TAttribute">The type of attribute.</typeparam> /// <typeparam name="TResolver">The type of resolver.</typeparam> /// <param name="resolver">The instance of resolver to register.</param> public static void RegisterResolver<TAttribute, TResolver>(TResolver resolver) where TAttribute : ValidationAttribute where TResolver : IAttributeResolver { var type = typeof(TAttribute); if (Resolvers.ContainsKey(type)) Resolvers[type] = resolver; else Resolvers.Add(type, resolver); } /// <summary> /// Registers any default resolvers. /// </summary> private static void RegisterDefaultResolvers() { RegisterResolver<RequiredAttribute, SimpleAttributeResolver>(new SimpleAttributeResolver("@Required")); RegisterResolver<EmailAttribute, SimpleAttributeResolver>(new SimpleAttributeResolver("@Email")); RegisterResolver<RangeAttribute, RangeAttributeResolver>(); } /// <summary> /// Gets a dictionary of constraints for the specified type. /// </summary> /// <returns>A dictionary of constraints for the specified type.</returns> public IEnumerable<string> GetConstraints(string property = null) { var constraints = new List<string>(); var attributes = GetAttributes(); if (!string.IsNullOrEmpty(property)) attributes = attributes.Concat(GetAttributes(property)); foreach (var attr in attributes) { Type type = attr.GetType(); if (Resolvers.ContainsKey(type)) { var resolver = Resolvers[type]; constraints.Add(resolver.Resolve(attr)); } } return constraints; } /// <summary> /// Gets the attributes applied to the specified model. /// </summary> /// <returns>An enumerable of validation attributes.</returns> private IEnumerable<ValidationAttribute> GetAttributes() { return ModelType .GetCustomAttributes(typeof(ValidationAttribute), true) .Cast<ValidationAttribute>(); } /// <summary> /// Gets the attributes applied to the specified property. /// </summary> /// <returns>An enumerable of validation attributes.</returns> private IEnumerable<ValidationAttribute> GetAttributes(string property) { return ModelType .GetProperty(property) .GetCustomAttributes(typeof(ValidationAttribute), true) .Cast<ValidationAttribute>(); } #endregion
Code language: PHP (php)

}
With that, we can now add a HtmlHelper extension method that will create our textbox:

/// /// Provides html helper extensions used to create html controls with Regula validation. ///
public static class RegulaExtensions
{
#region Methods
/// /// Creates a textbox for the specified expression that supports Regula validation. ///
/// The model type.
/// The property type.
/// The html helper used to generate the control.
/// The expression used to identify the property to create the control for.
/// The collection of html attributes to render to the client.
/// The string representation of the textbox.
public static MvcHtmlString RegulaTextBoxFor
(this HtmlHelper htmlHelper, Expression> expression, IDictionary htmlAttributes)
{
if (htmlAttributes == null)
{
htmlAttributes = new Dictionary { { “class”, “regula-validation” } };
}
else
{
if (htmlAttributes.ContainsKey(“class”))
htmlAttributes[“class”] = string.Join(” “, htmlAttributes[“class”] as string, “regula-validation”);
else
htmlAttributes.Add(“class”, “regula-validation”);
}

var adapter = new RegulaAdapter<TModel>(); var member = expression.Body as MemberExpression; string name = member == null ? null : member.Member.Name; var constraints = adapter.GetConstraints(name); htmlAttributes.Add("data-constraints", string.Join(" ", constraints)); return htmlHelper.TextBoxFor(expression, htmlAttributes); } /// <summary> /// Creates a textbox for the specified expression that supports Regula validation. /// </summary> /// <typeparam name="TModel">The model type.</typeparam> /// <typeparam name="TProperty">The property type.</typeparam> /// <param name="htmlHelper">The html helper used to generate the control.</param> /// <param name="expression">The expression used to identify the property to create the control for.</param> /// <param name="htmlAttributes">The object that represents the collection of html attributes.</param> /// <returns>The string representation of the textbox.</returns> public static MvcHtmlString RegulaTextBoxFor<TModel, TProperty> (this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object htmlAttributes) { return RegulaTextBoxFor(htmlHelper, expression, new RouteValueDictionary(htmlAttributes)); } /// <summary> /// Creates a textbox for the specified expression that supports Regula validation. /// </summary> /// <typeparam name="TModel">The model type.</typeparam> /// <typeparam name="TProperty">The property type.</typeparam> /// <param name="htmlHelper">The html helper used to generate the control.</param> /// <param name="expression">The expression used to identify the property to create the control for.</param> /// <returns>The string representation of the textbox.</returns> public static MvcHtmlString RegulaTextBoxFor<TModel, TProperty> (this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression) { return RegulaTextBoxFor(htmlHelper, expression, null); } #endregion
Code language: PHP (php)

}
Example Model with Client-side Validation
As an example, we’ll modify the stock ASP.NET MVC project. Let’s create an example model:

public class Person
{
#region Properties
[Required]
public string Name { get; set; }

[Email] public string Email { get; set; } [Range(0, 120)] public int Age { get; set; } #endregion
Code language: JavaScript (javascript)

}
By passing a dummy instance to our view, we can express the validation using something similar to:

<%: ViewData[“Message”] %>

To learn more about ASP.NET MVC visit <% Html.BeginForm(“Index”, “Home”, FormMethod.Post, new { id = “myForm” }); %>

<%= Html.RegulaTextBoxFor(m => m.Name) %>

<%= Html.RegulaTextBoxFor(m => m.Email) %>

<%= Html.RegulaTextBoxFor(m => m.Age) %>


<% Html.EndForm(); %>
Which should generate the following html markup:

Welcome to ASP.NET MVC!

To learn more about ASP.NET MVC visit

<div id="footer"> </div> </div>
Code language: HTML, XML (xml)


As I said, it is possible, and it also makes me wonder if this could be adapted to work with something like xVal? Who knows! Please find the example project attached…

RegulaMVC

Leave a Reply

Your email address will not be published.

Related Post