Heads up!
The blog has moved!
The new URL to bookmark is http://blog.valeriogheri.com/
What is a custom data attribute
“As per HTML 5 draft specification a custom data attribute is an attribute in no namespace whose name starts with the string “data-“, has at least one character after the hyphen, is XML-compatible, and contains no characters in the range U+0041 to U+005A (LATIN CAPITAL LETTER A to LATIN CAPITAL LETTER Z). Custom data attributes are intended to store custom data private to the page or application, for which there are no more appropriate attributes or elements. These attributes are not intended for use by software that is independent of the site that uses the attributes. It would be inappropriate, however, for the user to use generic software not associated with that music site to search for tracks of a certain length by looking at this data. This is because these attributes are intended for use by the site’s own scripts, and are not a generic extension mechanism for publicly-usable metadata. Every HTML element may have any number of custom data attributes specified, with any value. JavaScript libraries may use the custom data attributes, as they are considered to be part of the page on which they are used.”
This allows you to write valid HTML markup (passing an HTML 5 validator) while, simultaneously, embedding data within your page.
MVC 3 + jQuery
MVC3’s new jQuery Validation mechanism links jQuery Validation and Validation Attributes Metadata. The magic happens in the jquery.validate.unobtrusive file that takes all data- attributes and works with them.
To exploit this mechanism, we need to create our own Custom Validation Attributes as we’ll see in this article.
How MVC generates the data- attributes:
For each client validation rule(we’ll get there), an attribute is added with data-val-rulename=”message”. Then, for each parameter in the client validation rule, an attribute is added with data-val-rulename-paramname=”paramvalue”.
The tutorial
Step 1: Server side validation
First we create the model
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Web.Mvc; namespace DateCustomValidationExample.Models { public class Project { public string Name { get; set; } public string ProjectManager { get; set; } [DisplayName("Start date")] [DisplayFormat(DataFormatString = "{0:MM/dd/yyyy}", ApplyFormatInEditMode = true)] public DateTime StartDate { get; set; } [DisplayName("Estimated end date")] [DisplayFormat(DataFormatString = "{0:MM/dd/yyyy}", ApplyFormatInEditMode = true)] [DateGreaterThan("StartDate", "Estimated end date must be greater than the start date of the project")] public DateTime EndDate { get; set; } } }
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.ComponentModel.DataAnnotations; using System.Web.Mvc; namespace DateCustomValidationExample.Models { [AttributeUsage(AttributeTargets.Property, AllowMultiple=true)] public class DateGreaterThanAttribute : ValidationAttribute { string otherPropertyName; public DateGreaterThanAttribute(string otherPropertyName, string errorMessage) : base(errorMessage) { this.otherPropertyName = otherPropertyName; } protected override ValidationResult IsValid(object value, ValidationContext validationContext) { ValidationResult validationResult = ValidationResult.Success; try { // Using reflection we can get a reference to the other date property, in this example the project start date var otherPropertyInfo = validationContext.ObjectType.GetProperty(this.otherPropertyName); // Let's check that otherProperty is of type DateTime as we expect it to be if (otherPropertyInfo.PropertyType.Equals(new DateTime().GetType())) { DateTime toValidate = (DateTime)value; DateTime referenceProperty = (DateTime)otherPropertyInfo.GetValue(validationContext.ObjectInstance, null); // if the end date is lower than the start date, than the validationResult will be set to false and return // a properly formatted error message if (toValidate.CompareTo(referenceProperty) < 1) { validationResult = new ValidationResult(ErrorMessageString); } } else { validationResult = new ValidationResult("An error occurred while validating the property. OtherProperty is not of type DateTime"); } } catch (Exception ex) { // Do stuff, i.e. log the exception // Let it go through the upper levels, something bad happened throw ex; } return validationResult; } } }
Notice how the attribute extends ValidationAttribute and overrides IsValid method, in which we perform the custom validation.
Now we need aController and aView to test the validation.
In order to use it, we must add the following references to our page (or into the master layout page)
<script src="../../Scripts/jquery-1.x.x.js" type="text/javascript"></script> <script src="../../Scripts/jquery-ui-1.x.x.js" type="text/javascript"></script> <link href="../../Content/themes/base/jquery.ui.all.css" rel="stylesheet" type="text/css" />
<script> $().ready(function () { $("#StartDate").datepicker(); $("#EndDate").datepicker(); return false; }); </script>
Then, in order to use server side validation, we need to add the following code to our HomeController to handle form submit
// POST: /Home/ [HttpPost] public ActionResult Index(Project model) { return View(model); }
As expected there is no custom data attribute related to our custom rule.
Client side validation
Before we start, a few important notes (taken directly from the blog of one of the developers of Asp.Net MVC 3)
jQuery Validate requires your input elements to be inside of a <form> element in order to be validated. In addition, MVC 3 requires that you have called Html.BeginForm() to render this form, so that it can find its book-keeping object to help render the HTML attributes.
Writing a client-side validator involves two steps:
- adding one more method to our DateGreaterThanAttribute
- writing some jQuery code to create a validation rule and what is called an adapter, which takes the parameter values from the HTML attributes and turns it into jQuery Validate metadata.
Since we are implementing it, we need to write the method GetClientValidationRules like the following:
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context) { //string errorMessage = this.FormatErrorMessage(metadata.DisplayName); string errorMessage = ErrorMessageString; // The value we set here are needed by the jQuery adapter ModelClientValidationRule dateGreaterThanRule = new ModelClientValidationRule(); dateGreaterThanRule.ErrorMessage = errorMessage; dateGreaterThanRule.ValidationType = "dategreaterthan"; // This is the name the jQuery adapter will use //"otherpropertyname" is the name of the jQuery parameter for the adapter, must be LOWERCASE! dateGreaterThanRule.ValidationParameters.Add("otherpropertyname", otherPropertyName); yield return dateGreaterThanRule; }
Important: The metadata expressed for this rule will be used by the runtime to emit the HTML5 data-val custom attributes!
Let’s create a new Folder into /Scripts and call it “Custom”, then let’s create a new script called CustomValidation.js.
/* File Created: January 16, 2012 */ // Value is the element to be validated, params is the array of name/value pairs of the parameters extracted from the HTML, element is the HTML element that the validator is attached to $.validator.addMethod("dategreaterthan", function (value, element, params) { return Date.parse(value) > Date.parse($(params).val()); }); /* The adapter signature: adapterName is the name of the adapter, and matches the name of the rule in the HTML element. params is an array of parameter names that you're expecting in the HTML attributes, and is optional. If it is not provided, then it is presumed that the validator has no parameters. fn is a function which is called to adapt the HTML attribute values into jQuery Validate rules and messages. The function will receive a single parameter which is an options object with the following values in it: element The HTML element that the validator is attached to form The HTML form element message The message string extract from the HTML attribute params The array of name/value pairs of the parameters extracted from the HTML attributes rules The jQuery rules array for this HTML element. The adapter is expected to add item(s) to this rules array for the specific jQuery Validate validators that it wants to attach. The name is the name of the jQuery Validate rule, and the value is the parameter values for the jQuery Validate rule. messages The jQuery messages array for this HTML element. The adapter is expected to add item(s) to this messages array for the specific jQuery Validate validators that it wants to attach, if it wants a custom error message for this rule. The name is the name of the jQuery Validate rule, and the value is the custom message to be displayed when the rule is violated. */ $.validator.unobtrusive.adapters.add("dategreaterthan", ["otherpropertyname"], function (options) { options.rules["dategreaterthan"] = "#" + options.params.otherpropertyname; options.messages["dategreaterthan"] = options.message; });
$.validator.addMethod("dategreaterthan", function (value, element, params) { return Date.parse(value) > Date.parse($(params).val()); });
The adapter instead is tightly coupled with GetClientValidationRules implementation, therefore the first parameter must match the name we used in the code of our custom attribute for the validationType.
The second parameter is an array of parameters name.
In our case we only added one parameter
dateGreaterThanRule.ValidationParameters.Add("otherpropertyname", otherPropertyName);
options.rules["dategreaterthan"] //"dategreaterthan", the key to identify the rule, must match what we used in the addMethod as first parameter.
options.rules["dategreaterthan"] = "#" + options.params.otherpropertyname;
we use the # so that the value stored will be something like “#StartDate”, that is ready to be consumed by the validation rule previously wrote to get the value out of the StartDate field.
<script src="../../Scripts/jquery.validate.js" type="text/javascript"></script> <script src="../../Scripts/jquery.validate.unobtrusive.js" type="text/javascript"></script> <script src="../../Scripts/MicrosoftMvcAjax.js" type="text/javascript"></script> <script src="../../Scripts/Custom/CustomValidation.js" type="text/javascript"></script>
Everything’s done and now we can run the app. If we inspect the HTML that has been emitted this is what we see
and if we run the application we will see the same error message as before, with no form submit!
The source code is available on my github at the following URL: https://github.com/vgheri/DateCustomValidationExample
That’s all for the unobtrusive custom validation with MVC 3.
I hope this helps,
Valerio
Thanks for sharing your info. I really appreciate your efforts and I will be
waiting for your further write ups thank you once
again.
Thank you so much!
thank’s men
Would this work if using Ajax.BeginForm or does this method rely on HTML.BeginForm?
Hi, that’s an interesting question. I never used Ajax.BeginForm so I cannot be sure, but I guess that the javascript form validators should run just fine even if using Ajax.BeginForm.
Actually, it works fine with Ajax.BeginForm
Hello ,
I have implemented above example and found issue with jquery.validate.min version .
I was already using jQuery Validation Plugin – v1.11.0 – 2/4/2013 but above code is not working for me. When i am using jQuery Validation Plugin jquery.validate.min 1.8.0 it started working it surprised for me that downgrade version is working and upgraded not working . Also i don’t what are the implication or side effect of using 1.8.0 (might somewhere not working ) so please help me out on this whether i have need to use 1.8.0 or 1.11.0 without any impact?
Please someone can help me on this ASAP.
Thanks
Jay
This is really great man!!!! Thanks a lot!!!!
I’m glad this helped!
very nicely written!
Hello. Client side validation wasn’t working for me until I added the following on line 11 of the DateGreaterThanAttribute class: “, IClientValidatable”
It’s September 2016 and this post is as relevant (and to the point) as when it was originally written. Thank you so very much!
Hey! I implemented this Attribute a while ago and it works great but now I am localising my application and I have not found a way to link the error message to a resx file.
And second of all: The Date.parse function doesn’t work with the date format dd.MM.yyyy
Would be glad if you could help me with that issue!