One of the best things with MVC is the built-in validation using Data Annotations, these special attributes are simply applied to a class or its property and validation simply just happens on both Client and Server side (taking into consideration you set yours up properly). There are lots of Data Annotations built in that you can use and the most popular ones are the Required, Range and Data Type Attributes but sometimes these are not enough so we need something more usable that fits well with our business logic. Good thing we can create our own custom validation by overriding methods in the Validation Attribute class as well as with IClientValidatable.
So how do we do that? Lets start with an example, lets say you want to validate a users age by default you cannot use the built-in validators so you will have to create your own validation. Lets say you have a registration form like this screenshot below and you want to restrict sign ups for people only aged between 15 to 65 years old.
First lets create your own custom validation, let us call it ValidateAgeAttribute
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] public class ValidateAgeAttribute : ValidationAttribute, IClientValidatable { private const string DefaultErrorMessage = "Your age is invalid, your {0} should fall between {1} and {2}"; public DateTime MinimumDateProperty { get; private set; } public DateTime MaximumDateProperty { get; private set; } public ValidateAgeAttribute( int minimumAgeProperty, int maximumAgeProperty) : base(DefaultErrorMessage) { MaximumDateProperty = DateTime.Now.AddYears(minimumAgeProperty * -1); MinimumDateProperty = DateTime.Now.AddYears(maximumAgeProperty * -1); } protected override ValidationResult IsValid(object value, ValidationContext validationContext) { if (value != null) { DateTime parsedValue = (DateTime)value; if (parsedValue <= MinimumDateProperty || parsedValue >= MaximumDateProperty) { return new ValidationResult(FormatErrorMessage(validationContext.DisplayName)); } } return ValidationResult.Success; } public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context) { var rule = new ModelClientValidationRule() { ValidationType = "validateage", ErrorMessage = FormatErrorMessage(metadata.GetDisplayName()), }; rule.ValidationParameters.Add("minumumdate", MinimumDateProperty.ToShortDateString()); rule.ValidationParameters.Add("maximumdate", MaximumDateProperty.ToShortDateString()); yield return rule; } public override string FormatErrorMessage(string name) { return string.Format(ErrorMessageString, name, MinimumDateProperty.ToShortDateString(), MaximumDateProperty.ToShortDateString()); } }
In this class you notice you have three methods, first is IsValid. We override this method from the ValidationAttribute and this basically validates the specified value with respect to the current validation attribute, it outputs an instance of the System.ComponentModel.DataAnnotations.ValidationResult class. The next method is FormatErrorMessage which is from the same ValidationAttribute overriden class, as the name suggests this gets or sets an error message to associate with a validation control if validation. Now we have GetClientValidationRules which come from IClientValidatable where it provides a way for the ASP.NET MVC validation framework to discover at run time whether a validator has support for client validation.
Now let use this in your view model
Lets say your Birth day field is called Birthdate, we want it to be required and we want it to display as “Date of Birth”.
[DisplayName("Date of Birth")] [Required] [ValidateAge(15, 65)] public DateTime? BirthDate { get; set; }
Now if you notice you have 2 parameters in Validate Age 15 and 65, this is the property you set on minimumAgeProperty and maximumAgeProperty of your ValidateAgeAttribute class. You will also notice we use ValidateAge and not ValidateAgeAttribute, you can use both as it auto maps with the class.
At this point your server-side validation will now work all you need to do is before doing anything check the ModelState in your controller like this
[AcceptVerbs(HttpVerbs.Post)] [Transaction] public ActionResult CreateAccount(RegisterPageViewModel viewModel) { var errorMessage = string.Empty; if (ModelState.IsValid) { var newExtendedUserDetails = activeDirectoryTasks.CreateUser( viewModel.FirstName, viewModel.LastName, viewModel.Email, viewModel.BirthDate.Value, viewModel.SecretQuestion1Id, viewModel.SecretQuestionAnswer1, viewModel.SecretQuestion2Id, viewModel.SecretQuestionAnswer2, out errorMessage ); if (errorMessage == string.Empty) { //Success viewModel = registerQuery.GetSuccessPageViewModel(viewModel); return View("Success", viewModel); } //Error on Save viewModel = registerQuery.GetErrorPageViewModel(viewModel); return View("Error", viewModel); } //Validation Error viewModel = registerQuery.GetRegisterPageViewModel(viewModel); return View("Index", viewModel); }
So if you turn off your JavaScript and run then it should throw a validation error like such.
Now lets enable your Unobtrusive Validation, first is you need to create a separate js file. I called mine custom-validators.js and I include all my custom validators there. Inside that fill would be something like this
// The validateage function $.validator.addMethod( 'validateage', function (value, element, params) { return Date.parse(value) >= Date.parse(params.minumumdate) && Date.parse(value) <= Date.parse(params.maximumdate); }); $.validator.unobtrusive.adapters.add( 'validateage', ['minumumdate', 'maximumdate'], function (options) { var params = { minumumdate: options.params.minumumdate, maximumdate: options.params.maximumdate }; options.rules['validateage'] = params; options.messages['validateage'] = options.message; });
First function just adds the method and the second adds the unobtrusive adapter. Take note of the names as this is the one you declared early on your ModelClientValidationRule on the ValidateAgeAttribute.
Next is to make sure you reference to the following JavaScript in this specific order.
<script src="@Url.Content("~/Scripts/jquery-2.0.3.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/custom-validators.js")" type="text/javascript"></script>
And if you are using Bundle Collections then it will be like this
bundles.Add(new ScriptBundle("~/bundles/jquery") .Include("~/scripts/jquery-2.0.3.min.js") .Include("~/scripts/jquery.validate.min.js") .Include("~/scripts/jquery.validate.unobtrusive.min.js") .Include("~/scripts/custom-validators.js") );
and if you’re using Bundle Collection clear the default ordering as it will sort it alphabetically, you can do so by calling FileSetOrderList.Clear() after all the bundles have been created.
bundles.FileSetOrderList.Clear();
Dont also forget to put this on your web.config
<appSettings> <add key="ClientValidationEnabled" value="true" /> <add key="UnobtrusiveJavaScriptEnabled" value="true" />
Now lets see if everthing works fine, code behind should show your data-val’s created from your attribute
And it should do its validation
Awsome article!
Nice work. I’m surprised more people are not talking about pragmatic approaches like this.
Amazing article! Do you happen to know how to do one for date ranges? Ex: StartDate < EndDate.
Im having an issue with the javascript validation. Ive added as described above but when you enter a date that is outside the range it does not show the error message until you post back to server. I was hoping that it would alert dynamically like the password or invalid email address does on registration
You can refer this (http://amitergupta.blogspot.in/2015/10/mvc-date-range-validation-with-data.html) article which demonstrate server and client side validation with support of min nd max value as constants or dependent properties.
I would like to view the source code. The part with the validation summary in javascript.