This is a continuation of an earlier post about a nice framework that ties JQuery validation and the MS Validation Application block
See the solution here
http://www.codeplex.com/aspmvcvalidation.
Now what I wanted to do was extend this framework so I could generate the required client side regex validation methods based upon the tagging of class attributes. Do note that the currenframework does support RegExValidators but with a dependency. The dependency is that there is an external js file which defines the custom client side regex validation methods. For example a method defined in an external file like:
$.validator.addMethod('ValidZip',
function (value) {
return /^((\d{5}-\d{4})|(\d{5})|([A-Z]\d[A-Z]\s\d[A-Z]\d))$/.test(value);},
'Please enter a valid.');
When we have an external file that defines this custom method, we can tag our attributes like the following and the framework will work.
[RegularExpressionValidatorAttribute(
MessageTemplate = "Invalid zip code",
ClientFunctionName = "ValidZip",
Pattern = "Insert your Regex here")]
public string Zip { get; set; }
When this tag runs through the framework it will generate the following code for us.
$("#Zip").rules("add", {
ValidZip: true
messages: {
ValidZip: "Please enter a valid zip."
}
});
^((\d{5}-\d{4})|(\d{5})|([A-Z]\d[A-Z]\s\d[A-Z]\d))
This is all well and good but it assumes we have a custom method defined in an external file for the ValidZip RegEx check.
What I wanted to do was generate the methods and the rules based on the tag. The reason being is we already have the regex in the attribute tag and the JQuery code required to implement the custom client method is predictable. The benefit of generating the custom method is that I don't need to concern myself with keeping an external js file in sync with my attribute tags. Simply put the framework will do that for me.
In order to accomplish this I created another validator called ClientRegExValidator. What I do is decorate my attribute with something like the following
[ClientRegExValidator(@"/^[a-zA-Z0-9]+$/", "Password must contain only letters or numbers", "Password")]
public string Password { get; set; }
From here both the client side method and the rule definition will be generated by the JQueryGenerator class. Here is the changes I made to the framework.
ClientRegExValidatorAttribute class.
using System;
using Microsoft.Practices.EnterpriseLibrary.Validation;
using Microsoft.Practices.EnterpriseLibrary.Validation.Validators;
namespace Mvc.Validation.Validators
{
[AttributeUsage(AttributeTargets.Property
| AttributeTargets.Field
| AttributeTargets.Method
| AttributeTargets.Class
| AttributeTargets.Parameter,
AllowMultiple = false,
Inherited = false)]
public sealed class ClientRegExValidatorAttribute : ValueValidatorAttribute
{
string _ClientPattern;
string _FieldName;
string _MessageTemplate;
public ClientRegExValidatorAttribute(string MessageTemplate)
: base()
{
this._MessageTemplate = MessageTemplate;
}
public ClientRegExValidatorAttribute(string Pattern, string MessageTemplate, string FieldName)
: base()
{
this._FieldName = FieldName;
this._ClientPattern = Pattern;
this._MessageTemplate = MessageTemplate;
}
public string ClientPattern {
get
{
return this._ClientPattern;
}
set
{
}
}
public string FieldName {
get
{
return this._FieldName;
}
set
{
}
}
public string MessageTemplate
{
get
{
return this._MessageTemplate;
}
set
{
}
}
protected override Validator DoCreateValidator(Type targetType)
{
return new ClientRegExValidator( this._ClientPattern,this._MessageTemplate, this._FieldName);
}
}
}
ClientRegExValidator class
using System;
using Microsoft.Practices.EnterpriseLibrary.Validation;
using Microsoft.Practices.EnterpriseLibrary.Validation.Validators;
namespace Mvc.Validation.Validators
{
///
/// Logs an error if the string to validate is or empty.
///
public class ClientRegExValidator : ValueValidator
{
private string _Pattern;
private string _FieldName;
public ClientRegExValidator()
: this(null,null,null)
{ }
public ClientRegExValidator(string Pattern, string messageTemplate, string FieldName)
: base(messageTemplate, null, false)
{
this._FieldName = FieldName;
this._Pattern = Pattern;
}
protected override void DoValidate(object objectToValidate,
object currentTarget,
string key,
ValidationResults validationResults)
{
string stringValue = null;
if (objectToValidate != null && !(objectToValidate is string))
{
stringValue = Convert.ToString(objectToValidate);
}
else
{
stringValue = (string)objectToValidate;
}
}
protected override string DefaultNonNegatedMessageTemplate
{
get { return "Failed validation"; }
}
protected override string DefaultNegatedMessageTemplate
{
get { return null; }
}
}
}
JQueryCodeGenerator class
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Text;
using Microsoft.Practices.EnterpriseLibrary.Validation.Validators;
using Mvc.Validation.Validators;
namespace Mvc.Validation
{
///
/// Class to generate corresponding client-side validation code for an VAB-annotated
/// type.
///
/// The generated JavaScript code targets the jQuery validation plugin:
/// http://bassistance.de/jquery-plugins/jquery-plugin-validation/.
///
internal sealed class JQueryCodeGenerator : IValidationCodeGenerator
{
#region Implementing IValidationCodeGenerator
StringBuilder customFunctions = new StringBuilder();
public string GenerateCode(string formName, bool ignoreMissingElements, string FunctionName)
{
var body = GenerateScriptBody(ignoreMissingElements);
return GenerateScriptWrapper(body, formName, FunctionName);
}
#endregion
#region Private Members
///
/// Generates the body of the script by inspecting all properties and their
/// attributes.
///
private static string GenerateScriptBody(bool ignoreMissingElements)
{
var script = new StringBuilder();
var props = TypeDescriptor.GetProperties(typeof(T));
var functions = new StringBuilder();
foreach (PropertyDescriptor prop in props)
{
var rules = GetRulesOfProperty(prop);
var propertyScript = GenerateScriptForRules(prop, rules, ignoreMissingElements);
script.Append(propertyScript).AppendLine().AppendLine();
var functionScript = GenerateFunctionsForRules(prop, rules, ignoreMissingElements);
functions.Append(functionScript);
}
script.Append(functions.ToString());
return script.ToString();
}
private static string GetCustomFunctions(BaseValidationAttribute rule)
{
StringBuilder aFunction = new StringBuilder();
if (rule is ClientRegExValidatorAttribute)
{
ClientRegExValidatorAttribute localrule;
localrule = (ClientRegExValidatorAttribute)rule;
aFunction.Append("$.validator.addMethod('" + localrule.FieldName + "', function (value) { ");
aFunction.Append("return " + localrule.ClientPattern + ".test(value); ");
aFunction.Append("}, '" + rule.MessageTemplate + "');");
return aFunction.ToString();
}
return "";
//throw new NotSupportedException("Not supported attribute of type " + rule.GetType());
}
///
/// Generates the wrapper and initialization JS code for the validation.
///
private static string GenerateScriptWrapper(string bodyScript, string formName, string FunctionName)
{
var script = new StringBuilder();
script.Append("function " + FunctionName + "(){ \n");
if (!string.IsNullOrEmpty(formName))
{
//script.AppendFormat("$(\"#{0}\").validate( { errorLabelContainer: $(\"#{0}\" div.error });", formName).AppendLine();
script.Append("$(\"#" + formName + "\").validate( { errorLabelContainer: \"#" + formName + "_ErrorMessages\", wrapper: \"li\" });\n");
}
script.AppendLine().Append(bodyScript);
script.Append("}\n");
return string.Format("",
script, Environment.NewLine);
}
///
/// Retrieves all the validation attributes for the specified
///
private static IList GetRulesOfProperty(MemberDescriptor prop)
{
return prop.Attributes.OfType().ToList();
}
///
/// Generates the validation script for the specified
///
private static string GenerateFunctionsForRules(MemberDescriptor prop,
IList rules, bool ignoreMissingElements)
{
if (rules.Count == 0)
return string.Empty;
var functionScripts = new StringBuilder();
for (var i = 0; i < rules.Count; i++)
{
string aFunction = GetCustomFunctions(rules[i]);
functionScripts.Append(aFunction);
functionScripts.AppendLine();
}
return functionScripts.ToString();
}
///
/// Generates the validation script for the specified
///
private static string GenerateScriptForRules(MemberDescriptor prop,
IList rules, bool ignoreMissingElements)
{
if (rules.Count == 0)
return string.Empty;
var rulesStr = new StringBuilder();
var messagesStr = new StringBuilder();
if (ignoreMissingElements)
{
rulesStr.AppendFormat("if($(\"#{0}\").length > 0) {1}",
prop.Name, "{").AppendLine();
}
rulesStr.AppendFormat("$(\"#{0}\").rules(\"add\", {1}",
prop.Name, "{").AppendLine();
messagesStr.AppendFormat("\tmessages: {0}", "{").AppendLine();
for (var i = 0; i < rules.Count; i++)
{
ScriptInfo scriptInfo = GetRuleScript(rules[i]);
rulesStr.AppendFormat("\t{0},", scriptInfo.RuleScript).AppendLine();
messagesStr.AppendFormat("\t\t{0}", scriptInfo.MessageScript);
if (i < rules.Count - 1)
{
messagesStr.Append(",");
}
messagesStr.AppendLine();
}
messagesStr.AppendFormat("\t{0}", "}").AppendLine();
rulesStr.Append(messagesStr.ToString());
rulesStr.AppendFormat("{0});", "}");
if (ignoreMissingElements)
{
rulesStr.AppendFormat("{0}", "}");
}
return rulesStr.ToString();
}
private static ScriptInfo GetRuleScript(BaseValidationAttribute rule)
{
if (rule is BaseValueValidatorAttribute)
{
var customRule = rule as BaseValueValidatorAttribute;
var ruleName = customRule.ClientFunctionName;
return new ScriptInfo
{
RuleScript = ruleName + ": true",
MessageScript = string.Format("{0}: \"{1}\"", ruleName, customRule.FormattedMessageTemplate)
};
}
if (rule is NotNullOrEmptyValidatorAttribute ||
rule is NotNullValidatorAttribute)
{
return new ScriptInfo
{
RuleScript = "required: true",
MessageScript = string.Format("required: \"{0}\"", rule.MessageTemplate)
};
}
if (rule is EmailValidatorAttribute)
{
return new ScriptInfo
{
RuleScript = "email: true",
MessageScript = string.Format("email: \"{0}\"", rule.MessageTemplate)
};
}
if (rule is StringLengthValidatorAttribute)
{
// lowerBound and upperBound fields are not visible
// thus need to use reflection to retrieve them
var lowerBound = GetField(rule, "lowerBound");
var upperBound = GetField(rule, "upperBound");
// To understand the magic numbers 3 & 5, refer here
// http://msdn.microsoft.com/en-us/library/cc511854.aspx
var errorMessage = rule.MessageTemplate
.Replace("{3}", lowerBound.ToString())
.Replace("{5}", upperBound.ToString());
return new ScriptInfo
{
RuleScript = string.Format("minlength: \"{0}\", maxlength : \"{1}\"",
lowerBound, upperBound),
MessageScript = string.Format("minlength: \"{0}\", maxlength : \"{0}\"",
errorMessage)
};
}
if (rule is RangeValidatorAttribute)
{
// lowerBound and upperBound fields are not visible
// thus need to use reflection to retrieve them
var lowerBound = GetField(rule, "lowerBound");
var upperBound = GetField(rule, "upperBound");
var errorMessage = rule.MessageTemplate;
return new ScriptInfo
{
RuleScript = string.Format("rangelength: [{0}, {1}]",
lowerBound, upperBound),
MessageScript = string.Format("required: \"{0}\"", errorMessage)
};
}
if (rule is ClientRegExValidatorAttribute)
{
ClientRegExValidatorAttribute localrule;
localrule = (ClientRegExValidatorAttribute)rule;
return new ScriptInfo
{
RuleScript = localrule.FieldName + ": true",
MessageScript = string.Format("{0}: \"{1}\"", localrule.FieldName, localrule.MessageTemplate)
};
}
if (rule is PropertyComparisonValidatorAttribute)
{
var propToCompare = GetField(rule, "propertyToCompare");
var propDescriptor = GetProperty(propToCompare);
var propOperator = GetField(rule, "comparisonOperator");
if (propOperator == ComparisonOperator.Equal)
{
return new ScriptInfo
{
RuleScript = string.Format("equalTo: \"#{0}\"", GetClientID(propDescriptor)),
MessageScript = string.Format("equalTo: \"{0}\"", rule.MessageTemplate)
};
}
}
throw new NotSupportedException("Not supported attribute of type " + rule.GetType());
}
private static T GetField(object obj, string fieldName)
{
var fieldInfo = obj.GetType().GetField(fieldName,
BindingFlags.Instance | BindingFlags.NonPublic);
return (T)fieldInfo.GetValue(obj);
}
private static PropertyDescriptor GetProperty(string name)
{
var props = TypeDescriptor.GetProperties(typeof(T));
return props.Find(name, false);
}
private static string GetClientID(MemberDescriptor prop)
{
var clientIdList = prop.Attributes.OfType().ToList();
return clientIdList.Count == 0
? prop.Name
: clientIdList[0].ClientId;
}
private class ScriptInfo
{
public string RuleScript { get; set; }
public string MessageScript { get; set; }
}
#endregion
}
}
So with this technique we can get our custom RegEx client validation methods set up on the fly. This removes the dependency on any external js files. Please note that this custom validator will only enforce client side validation. This is by design and the reason why I prefixed the validator name with "Client".

3 thoughts on “ASP.NET MVC and JQUERY Validation Part II”
Hahaha… this saved my butt on our favorite corporate site today. Thanks, dude!