Since this is something I expect to need, I wrote a reusable remote validator, based upon the remote validation example for MVC 2. The underlying issue is that the jquery.validate.js remote validator has no way to pass the value to be validated using a
generic name. Because of this, we need a new remote validation attribute, which can pass the value using a generic name, which is the variable name expected by our reusable remote function.
Here are the elements needed.
First, we have our example remote validation function, which in this case checks to see that our address fields do not have leading spaces (gee, a really useful out-of-the-box example, huh?). Create a ValidationController class with the following:
[OutputCache(Location = OutputCacheLocation.None, NoStore = true)]
[ControllerSessionState(System.Web.SessionState.SessionStateBehavior.Disabled)]
public class ValidationController : Controller
{
public JsonResult AddressCheck(string addresspart)
{
if (!addresspart.StartsWith(" "))
return Json(true, JsonRequestBehavior.AllowGet);
return Json("Cannot start address with a space", JsonRequestBehavior.AllowGet);
}
}
This assumes you are using MVC 3. If not, remove [ControllerSessionState(...)].
Next, add the following to jquery.unobtrusive.validate.js (for MVC 3) or MicrosoftMvcJqueryValidation.js for MVC 2 (though some changes are probably needed for MVC 2:
// reusable remote validation
$jQval.addMethod("remoteval", function (value, element, params) {
var url = params.url;
var parameterName = params.parametername;
var inputName = params.inputname;
if (this.optional(element))
return "dependency-mismatch";
var previous = this.previousValue(element);
if (!this.settings.messages[element.name])
this.settings.messages[element.name] = {};
previous.originalMessage = this.settings.messages[element.name].remoteval;
this.settings.messages[element.name].remoteval = previous.message;
if (element == document.activeElement) {
return true;
}
if (previous.old !== value) {
previous.old = value;
var validator = this;
this.startRequest(element);
var data = {};
// use the generic input name the remote function expects
// to hold the value to be validated
data[inputName] = value;
$.ajax({
url: url,
data: data,
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function (response) {
var valid = response === true;
if (valid) {
//var submitted = validator.formSubmitted;
//validator.prepareElement(element);
//validator.formSubmitted = submitted;
//validator.successList.push(element);
//validator.showErrors();
$.validationEngine.closePrompt(element, false);
} else {
//var errors = {};
var message = (previous.message = response || validator.defaultMessage(element, "remote"));
//errors[element.name] = $.isFunction(message) ? message(value) : message;
//validator.showErrors(errors);
$.validationEngine.buildPrompt(element, message, "error");
}
previous.valid = valid;
validator.stopRequest(element, valid);
return previous.valid;
}
});
return "pending";
} else if (this.pending[element.name]) {
return "pending";
}
return previous.valid;
});
This example is set up for the jquery.validationEngine plug-in, but the regular validator stuff is still there.
The following also must be added to the same js file:
adapters.add("remoteval", ["url", "parametername", "inputname"], function (options) {
var value = {
url: options.params.url,
parametername: options.params.parametername,
inputname: options.params.inputname
};
setValidationValues(options, "remoteval", value);
});
Next, we create our custom remote reusable validator:
// reusable remote validator
public sealed class RemoteReusableAttribute : ValidationAttribute
{
public string Action { get; set; }
public string Controller { get; set; }
// InputName is the generic name the remote validation function expects
public string InputName { get; set; }
public RemoteReusableAttribute()
{
}
public RemoteReusableAttribute(string action, string controller)
{
this.Action = action;
this.Controller = controller;
}
public RemoteReusableAttribute(string action, string controller, string inputName)
{
this.Action = action;
this.Controller = controller;
this.InputName = inputName;
}
public override bool IsValid(object value)
{
return true;
}
}
public class RemoteReusableAttributeAdapter : DataAnnotationsModelValidator<RemoteReusableAttribute>
{
public RemoteReusableAttributeAdapter(ModelMetadata metadata, ControllerContext context,
RemoteReusableAttribute attribute) :
base(metadata, context, attribute)
{
}
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
ModelClientValidationRule rule = new ModelClientValidationRule()
{
// Use the default DataAnnotationsModelValidator error message.
// This error message will be overridden by the string returned by
// the remote validation function.
ErrorMessage = ErrorMessage,
ValidationType = "remoteval"
};
rule.ValidationParameters["url"] = GetUrl();
rule.ValidationParameters["parametername"] = this.Metadata.PropertyName;
// Add the input name the remote validation function expects. If no
// generic input name is specified, use the name of the parameter to be validated.
rule.ValidationParameters["inputname"] =
String.IsNullOrEmpty(Attribute.InputName) ? this.Metadata.PropertyName : Attribute.InputName;
return new ModelClientValidationRule[] { rule };
}
private string GetUrl()
{
RouteValueDictionary rvd = new RouteValueDictionary() {
{ "controller", Attribute.Controller },
{ "action", Attribute.Action }
};
var virtualPath = RouteTable.Routes.GetVirtualPath(ControllerContext.RequestContext,
null, rvd);
if (virtualPath == null)
{
throw new InvalidOperationException("No route matched!");
}
return virtualPath.VirtualPath;
}
}
Then, we add the following to Application_Start() in global.asax.cs:
Then, finally, we add the following in our metadata class:
[DisplayName("Address")]
[RemoteReusable("AddressCheck", "Validation", "addresspart")]
[Required(ErrorMessage = "Address Required")]
[StringLength(63, MinimumLength = 2, ErrorMessage = "Must be between 2 and 63 characters long")]
public object Address1 { get; set; }
[DisplayName("Address - Line Two")]
[RemoteReusable("AddressCheck", "Validation", "addresspart")]
[StringLength(63, ErrorMessage = "Must be < 63 characters long")]
public object Address2 { get; set; }
Now, when we have a form with the Address1 and Address2 fields, both will make a request to the server to validate that the input does not start with a space, and both will pass the generic variable "addresspart" to our function "AddressCheck".
counsellorbe...
Member
540 Points
112 Posts
Re: CustomValidation?
Nov 30, 2010 12:14 AM|LINK
Since this is something I expect to need, I wrote a reusable remote validator, based upon the remote validation example for MVC 2. The underlying issue is that the jquery.validate.js remote validator has no way to pass the value to be validated using a generic name. Because of this, we need a new remote validation attribute, which can pass the value using a generic name, which is the variable name expected by our reusable remote function.
Here are the elements needed.
First, we have our example remote validation function, which in this case checks to see that our address fields do not have leading spaces (gee, a really useful out-of-the-box example, huh?). Create a ValidationController class with the following:
[OutputCache(Location = OutputCacheLocation.None, NoStore = true)] [ControllerSessionState(System.Web.SessionState.SessionStateBehavior.Disabled)] public class ValidationController : Controller { public JsonResult AddressCheck(string addresspart) { if (!addresspart.StartsWith(" ")) return Json(true, JsonRequestBehavior.AllowGet); return Json("Cannot start address with a space", JsonRequestBehavior.AllowGet); } }This assumes you are using MVC 3. If not, remove [ControllerSessionState(...)].
Next, add the following to jquery.unobtrusive.validate.js (for MVC 3) or MicrosoftMvcJqueryValidation.js for MVC 2 (though some changes are probably needed for MVC 2:
// reusable remote validation $jQval.addMethod("remoteval", function (value, element, params) { var url = params.url; var parameterName = params.parametername; var inputName = params.inputname; if (this.optional(element)) return "dependency-mismatch"; var previous = this.previousValue(element); if (!this.settings.messages[element.name]) this.settings.messages[element.name] = {}; previous.originalMessage = this.settings.messages[element.name].remoteval; this.settings.messages[element.name].remoteval = previous.message; if (element == document.activeElement) { return true; } if (previous.old !== value) { previous.old = value; var validator = this; this.startRequest(element); var data = {}; // use the generic input name the remote function expects // to hold the value to be validated data[inputName] = value; $.ajax({ url: url, data: data, contentType: "application/json; charset=utf-8", dataType: "json", success: function (response) { var valid = response === true; if (valid) { //var submitted = validator.formSubmitted; //validator.prepareElement(element); //validator.formSubmitted = submitted; //validator.successList.push(element); //validator.showErrors(); $.validationEngine.closePrompt(element, false); } else { //var errors = {}; var message = (previous.message = response || validator.defaultMessage(element, "remote")); //errors[element.name] = $.isFunction(message) ? message(value) : message; //validator.showErrors(errors); $.validationEngine.buildPrompt(element, message, "error"); } previous.valid = valid; validator.stopRequest(element, valid); return previous.valid; } }); return "pending"; } else if (this.pending[element.name]) { return "pending"; } return previous.valid; });This example is set up for the jquery.validationEngine plug-in, but the regular validator stuff is still there.
The following also must be added to the same js file:
adapters.add("remoteval", ["url", "parametername", "inputname"], function (options) { var value = { url: options.params.url, parametername: options.params.parametername, inputname: options.params.inputname }; setValidationValues(options, "remoteval", value); });Next, we create our custom remote reusable validator:
// reusable remote validator public sealed class RemoteReusableAttribute : ValidationAttribute { public string Action { get; set; } public string Controller { get; set; } // InputName is the generic name the remote validation function expects public string InputName { get; set; } public RemoteReusableAttribute() { } public RemoteReusableAttribute(string action, string controller) { this.Action = action; this.Controller = controller; } public RemoteReusableAttribute(string action, string controller, string inputName) { this.Action = action; this.Controller = controller; this.InputName = inputName; } public override bool IsValid(object value) { return true; } } public class RemoteReusableAttributeAdapter : DataAnnotationsModelValidator<RemoteReusableAttribute> { public RemoteReusableAttributeAdapter(ModelMetadata metadata, ControllerContext context, RemoteReusableAttribute attribute) : base(metadata, context, attribute) { } public override IEnumerable<ModelClientValidationRule> GetClientValidationRules() { ModelClientValidationRule rule = new ModelClientValidationRule() { // Use the default DataAnnotationsModelValidator error message. // This error message will be overridden by the string returned by // the remote validation function. ErrorMessage = ErrorMessage, ValidationType = "remoteval" }; rule.ValidationParameters["url"] = GetUrl(); rule.ValidationParameters["parametername"] = this.Metadata.PropertyName; // Add the input name the remote validation function expects. If no // generic input name is specified, use the name of the parameter to be validated. rule.ValidationParameters["inputname"] = String.IsNullOrEmpty(Attribute.InputName) ? this.Metadata.PropertyName : Attribute.InputName; return new ModelClientValidationRule[] { rule }; } private string GetUrl() { RouteValueDictionary rvd = new RouteValueDictionary() { { "controller", Attribute.Controller }, { "action", Attribute.Action } }; var virtualPath = RouteTable.Routes.GetVirtualPath(ControllerContext.RequestContext, null, rvd); if (virtualPath == null) { throw new InvalidOperationException("No route matched!"); } return virtualPath.VirtualPath; } }Then, we add the following to Application_Start() in global.asax.cs:
// reusable remote validator DataAnnotationsModelValidatorProvider.RegisterAdapter( typeof(FormsAway.Models.RemoteReusableAttribute), typeof(FormsAway.Models.RemoteReusableAttributeAdapter));Then, finally, we add the following in our metadata class:
[DisplayName("Address")] [RemoteReusable("AddressCheck", "Validation", "addresspart")] [Required(ErrorMessage = "Address Required")] [StringLength(63, MinimumLength = 2, ErrorMessage = "Must be between 2 and 63 characters long")] public object Address1 { get; set; } [DisplayName("Address - Line Two")] [RemoteReusable("AddressCheck", "Validation", "addresspart")] [StringLength(63, ErrorMessage = "Must be < 63 characters long")] public object Address2 { get; set; }Now, when we have a form with the Address1 and Address2 fields, both will make a request to the server to validate that the input does not start with a space, and both will pass the generic variable "addresspart" to our function "AddressCheck".
Q.E.D.
counsellorben