The mystery of the missing URL validation
Further to my adventures with unobtrusive validation of yesterday, I found myself very stuck while implementing a new form for this new editable metadata feature for the NuGet Gallery. And Yet again the cause of all my pain appears to be this evil combination of UnobtrusiveValidation (2.0.30116.0) and JQuery.Validation (1.8.1). (I know it's not meant to be an evil combination, but believe me, it really starts to feel that way.))
For the sake of discussion, a feature I'm trying to add is the ability to edit your package's project url, and logo icon url on nuget.org. Since I'm trying to follow the established patterns of the codebase, I decided to use a RequestModel object to represent the editing form. So naturally I do this inside my request model:
[StringLength(256)]
[Display(Name = "Icon URL")]
[DataType(DataType.ImageUrl)]
public string IconUrl { get; set; }
[StringLength(256)]
[Display(Name = "Project Homepage URL")]
[DataType(DataType.Url)]
public string ProjectUrl { get; set; }
Unfortunately, when it comes to validation of this model, woe is me. If you enter a string which is not a valid URL and doesn't even start with https, you get no indication you've done something stupid.
Perhaps naturally, perhaps not, I had assumed that adding these DataType attributes is going to do something good for me and do URL validation. Why would it be a natural assumption? Well, I say that because UnobtrusiveValidation appears to understand DataType .Url in the following ways:
[Updated: Gee thanks a lot blogs.msdn.com for corrupting my post. Now don't do it again!]
1) It adds a type="Url" attribute to your form INPUT element
2) It has code inside that tries to add an adapter which understands URL typed input elements and apply the jquery.validate URL validation to them. At least that's what I think it's meant to do. Because it doesn't actually work in practice.
I tried to work around this by adding regex validation to my model, thinking that could replace the non-functioning URL validation. Sadly that doesn't work either!
[RegularExpression(Constants.UrlValidationRegEx, ErrorMessage = "This doesn't appear to be a valid URL")]
Some hours of debugging eventually reveals that the reason neither regexes nor URLs are working is because I chose to do [DataType.Url] prevents the focusout delegate from being registered as a validation hook upon the input element
function delegate(event) {
var validator = $.data(this[0].form, "validator"),
eventType = "on" + event.type.replace(/^validate/, "");
validator.settings[eventType] && validator.settings[eventType].call(validator, this[0] );
}
$(this.currentForm)
.validateDelegate(":text, :password, :file, select, textarea", "focusin focusout keyup", delegate)
.validateDelegate(":radio, :checkbox, select, option", "click", delegate);
yes... jquery.validate will only add focusout handlers for stuff it knows about.
And it gives you no way of customizing this. Grrr. Yuck.
Best option for working around this seems to be don't use DataType.Url. But [update #2] unfortunately that will require me to modify my css styling which was based on detecting the url datatype.
So second good option is to write some (obtrusive!) script to add the missing focusout event handlers. This was yet another good 'learning exercise'.
$(function () {
var form = $('form[action="/packages/PoliteCaptcha/Edit/"]').toArray()[0];
$("input[type=url]").bind("focusin focusout keyup", function (event) {
var target = $(event.target);
var validator = $.data(form, "validator"),
eventType = "on" + event.type.replace(/^validate/, "");
validator.settings[eventType] && validator.settings[eventType].call(validator, event.target);
});
});
Things that were annoying to get right here:
-Figuring out that for whatever reason I can't just do $.bind() like jquery.validate does. I have to bind to a proper jquery element (collection).
-For the longest time $.data was returning me null and I couldn't figure out why until with the javascript debugger I realized there were two different definitions of the function at the different points in time because I was including jquery.min.js after jquery.js and jquery.validate.js. Owch!
-figuring out that the object to pass to the validator.settings[eventType] func was event.target also took me a while.