Comment valider une date en utilisant 3 menus déroulants (jour, mois, année) en utilisant la validation jquery non intrusive

J’ai un modèle à valider et le problème est la date de naissance. Il doit être composé de 3 menus déroulants (jour, mois, année).

@Html.LabelFor(m => m.DateOfBirth, new { @class = "label-div" }) @Html.Telerik().DropDownList().Name("DobDay").BindTo((SelectList)ViewData["Days"]).HtmlAtsortingbutes(new {id = "DobDaySel"}) @Html.Telerik().DropDownList().Name("DobMonth").BindTo((SelectList)ViewData["Months"]).HtmlAtsortingbutes(new { id = "DobMonthSel"}) @Html.Telerik().DropDownList().Name("DobYear").BindTo((SelectList)ViewData["Years"]).HtmlAtsortingbutes(new { id = "DobYearSel" }) @Html.ValidationMessageFor(m => m.DateOfBirth)

Du côté du serveur, je le fais

  [HttpPost] public ActionResult Register(RegistrationModel regInfo, int DobDay, int DobMonth, int DobYear) { SetRegisterViewData(DobDay, DobMonth, DobYear); if (DobDay == 0 || DobMonth == 0 && DobYear == 0) { ModelState.AddModelError("DateOfBirth", "Date of birth is required"); } else { DateTime dt = new DateTime(DobYear, DobMonth, DobDay); long ticks = DateTime.Now.Ticks - dt.Ticks; int years = new DateTime(ticks).Year; if (years < 18) { ModelState.AddModelError("DateOfBirth", "You must be at least 18"); } } if (ModelState.IsValid) { //register user return RedirectToAction("Index", "Home"); } return View(regInfo); } 

Des questions:

  1. Côté serveur: comment l’améliorer? (Je songe à append les propriétés dob, mois et année RegistrationModel et à append un atsortingbut à DateOfBirth pour vérifier ces propriétés)
  2. Côté client: je cherchais à effectuer une validation côté client pour un atsortingbut personnalisé, mais cela m’a dérouté. Quel est le moyen de le faire?

LE: J’ai créé un classeur de modèle personnalisé pour la date comme ceci:

  public class DobModelBinder : DefaultModelBinder { protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor) { if (propertyDescriptor.Name == "DateOfBirth") { DateTime dob = DateTime.MinValue; var form = controllerContext.HttpContext.Request.Form; int day = Convert.ToInt32(form["DobDay"]); int month = Convert.ToInt32(form["DobMonth"]); int year = Convert.ToInt32(form["DobYear"]); if (day == 0 || month == 0 || year == 0) { SetProperty(controllerContext, bindingContext, propertyDescriptor, DateTime.MinValue); } else { SetProperty(controllerContext, bindingContext, propertyDescriptor, new DateTime(year, month, day)); } } else { base.BindProperty(controllerContext, bindingContext, propertyDescriptor); } } } 

Je l’ai enregistré comme ça:

 ModelBinders.Binders.Add(typeof(DateTime), new DobModelBinder()); 

Je l’ai utilisé comme ça:

 public ActionResult Register([ModelBinder(typeof(DobModelBinder))]RegistrationModel regInfo) 

DateOfBirth se lie bien.

LE2:

J’ai créé des atsortingbuts de validation pour la date de naissance comme ceci:

  public override bool IsValid(object value) { DateTime date = Convert.ToDateTime(value); return date != DateTime.MinValue; } public IEnumerable GetClientValidationRules(ModelMetadata metadata, ControllerContext context) { yield return new ModelClientValidationRule { ErrorMessage = this.ErrorMessage, ValidationType = "dateRequired" }; } } public class DateGraterThanEighteen : ValidationAtsortingbute, IClientValidatable { public override bool IsValid(object value) { DateTime date = Convert.ToDateTime(value); long ticks = DateTime.Now.Ticks - date.Ticks; int years = new DateTime(ticks).Year; return years >= 18; } public IEnumerable GetClientValidationRules(ModelMetadata metadata, ControllerContext context) { yield return new ModelClientValidationRule { ErrorMessage = this.ErrorMessage, ValidationType = "dateGraterThanEighteen" }; } } 

J’ai appliqué des atsortingbuts comme celui-ci

  [DateGraterThanEighteen(ErrorMessage="You must be at least 18")] [DateRequired(ErrorMessage = "Date of birth is required")] public DateTime DateOfBirth { get; set; } 

LE3:

Du côté client, je fais ceci:

  $(function () { jQuery.validator.addMethod('dobRequired', function (value, element, params) { if (!/Invalid|NaN/.test(new Date(value))) { return true; } else { return false; } }, ''); jQuery.validator.unobtrusive.adapters.add('dateRequired', {}, function (options) { options.rules['dobRequired'] = true; options.messages['dobRequired'] = options.message; }); }); 

La validation du client ne semble pas fonctionner. Comment puis-je le réparer? Je suis un peu confus avec le fonctionnement de ces adaptateurs.

Vous pouvez utiliser un modèle d’éditeur personnalisé.

Voyons d’abord à quoi pourrait ressembler la solution finale avant d’entrer dans les détails de la mise en œuvre.

Ainsi, nous pourrions avoir (comme toujours) un modèle de vue orné d’atsortingbuts d’annotation de données indiquant les métadonnées que nous aimerions y attacher:

 public class MyViewModel { [DisplayName("Date of birth:")] [TrippleDDLDateTime(ErrorMessage = "Please select a valid DOB")] [Required(ErrorMessage = "Please select your DOB")] [MinAge(18, ErrorMessage = "You must be at least 18 years old")] public DateTime? Dob { get; set; } } 

alors nous pourrions avoir un contrôleur:

 public class HomeController : Controller { public ActionResult Index() { var model = new MyViewModel(); return View(model); } [HttpPost] public ActionResult Index(MyViewModel model) { if (!ModelState.IsValid) { return View(model); } return Content( ssortingng.Format( "Thank you for selecting your DOB: {0:yyyy-MM-dd}", model.Dob ) ); } } 

une vue ( ~/Views/Home/Index.cshtml ):

 @model MyViewModel @using (Html.BeginForm()) { @Html.EditorFor(x => x.Dob)  } 

et un modèle d’éditeur correspondant qui nous permettra d’afficher 3 listes déroulantes pour l’édition du champ DateTime au lieu d’une simple zone de texte ( ~/Views/Shared/EditorTemplates/TrippleDDLDateTime.cshtml ):

 @{ var now = DateTime.Now; var years = Enumerable.Range(0, 150).Select(x => new SelectListItem { Value = (now.Year - x).ToSsortingng(), Text = (now.Year - x).ToSsortingng() }); var months = Enumerable.Range(1, 12).Select(x => new SelectListItem { Value = x.ToSsortingng("00"), Text = x.ToSsortingng() }); var days = Enumerable.Range(1, 31).Select(x => new SelectListItem { Value = x.ToSsortingng("00"), Text = x.ToSsortingng() }); var result = ViewData.ModelState[ViewData.TemplateInfo.HtmlFieldPrefix]; if (result != null) { var values = result.Value.RawValue as ssortingng[]; years = new SelectList(years, "Value", "Text", values[0]); months = new SelectList(months, "Value", "Text", values[1]); days = new SelectList(days, "Value", "Text", values[2]); result.Value = null; } } 
@Html.Label("") @Html.DropDownList("", years, "-- year --") @Html.DropDownList("", months, "-- month --") @Html.DropDownList("", days, "-- day --") @Html.ValidationMessage("")

Voyons maintenant comment l’atsortingbut [TrippleDDLDateTime] pourrait être implémenté:

 public class TrippleDDLDateTimeAtsortingbute : ValidationAtsortingbute, IMetadataAware { public void OnMetadataCreated(ModelMetadata metadata) { metadata.TemplateHint = "TrippleDDLDateTime"; } public override bool IsValid(object value) { // It's the custom model binder that is responsible for validating return true; } } 

Notez comment l’atsortingbut implémente l’interface IMetadataAware , ce qui nous permet d’associer la propriété de modèle de vue au modèle d’éditeur personnalisé que nous avons écrit ( TrippleDDLDateTime.cshtml ).

Et ensuite vient l’atsortingbut [MinAge] :

 public class MinAgeAtsortingbute : ValidationAtsortingbute { private readonly int _minAge; public MinAgeAtsortingbute(int minAge) { _minAge = minAge; } public override bool IsValid(object value) { if (value == null) { return true; } DateTime date = Convert.ToDateTime(value); long ticks = DateTime.Now.Ticks - date.Ticks; int years = new DateTime(ticks).Year; return years >= _minAge; } } 

La dernière pièce du puzzle consiste à écrire un classeur de modèle personnalisé qui sera associé aux propriétés décorées avec l’atsortingbut [TrippleDDLDateTime] afin d’effectuer l’parsing syntaxique suivante:

 public class TrippleDDLDateTimeModelBinder : DefaultModelBinder { public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { var metadata = bindingContext.ModelMetadata; var sortingppleDdl = metadata.ContainerType.GetProperty(metadata.PropertyName).GetCustomAtsortingbutes(typeof(TrippleDDLDateTimeAtsortingbute), true).FirstOrDefault() as TrippleDDLDateTimeAtsortingbute; if (sortingppleDdl == null) { return base.BindModel(controllerContext, bindingContext); } var prefix = bindingContext.ModelName; var value = bindingContext.ValueProvider.GetValue(prefix); var parts = value.RawValue as ssortingng[]; if (parts.All(ssortingng.IsNullOrEmpty)) { return null; } bindingContext.ModelState.SetModelValue(prefix, value); var dateStr = ssortingng.Format("{0}-{1}-{2}", parts[0], parts[1], parts[2]); DateTime date; if (DateTime.TryParseExact(dateStr, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out date)) { return date; } bindingContext.ModelState.AddModelError(prefix, sortingppleDdl.ErrorMessage); return null; } } 

Notez que le classeur utilise simplement le classeur par défaut si le champ n’est pas décoré avec l’atsortingbut personnalisé. Ainsi, il n’interférera pas avec les autres champs DateTime pour lesquels nous ne voulons pas le comportement sortingpple ddl. Le classeur de modèle sera simplement associé à DateTime? tapez Application_Start :

 ModelBinders.Binders.Add(typeof(DateTime?), new TrippleDDLDateTimeModelBinder()); 

OK, nous avons jusqu’à présent une solution qui effectue la validation côté serveur. C’est toujours ce avec quoi vous devriez commencer. Parce que c’est là que vous pouvez également vous arrêter et toujours disposer d’un site sécurisé et fonctionnel.

Bien sûr, si vous avez le temps, vous pouvez maintenant améliorer l’expérience utilisateur en implémentant la validation côté client. La validation côté client n’est pas obligatoire, mais elle économise de la bande passante et évite les allers-retours du serveur.

Nous commençons donc par faire en sorte que nos 2 atsortingbuts personnalisés implémentent l’interface IClientValidatable , qui constitue la première étape de l’activation de la validation discrète côté client.

[TrippleDDLDateTime] :

 public class TrippleDDLDateTimeAtsortingbute : ValidationAtsortingbute, IMetadataAware, IClientValidatable { public void OnMetadataCreated(ModelMetadata metadata) { metadata.TemplateHint = "TrippleDDLDateTime"; } public override bool IsValid(object value) { // It's the custom model binder that is responsible for validating return true; } public IEnumerable GetClientValidationRules(ModelMetadata metadata, ControllerContext context) { var rule = new ModelClientValidationRule(); rule.ErrorMessage = ErrorMessage; rule.ValidationType = "sortingppleddldate"; yield return rule; } } 

[MinAge] :

 public class MinAgeAtsortingbute : ValidationAtsortingbute, IClientValidatable { private readonly int _minAge; public MinAgeAtsortingbute(int minAge) { _minAge = minAge; } public override bool IsValid(object value) { if (value == null) { return true; } DateTime date = Convert.ToDateTime(value); long ticks = DateTime.Now.Ticks - date.Ticks; int years = new DateTime(ticks).Year; return years >= _minAge; } public IEnumerable GetClientValidationRules(ModelMetadata metadata, ControllerContext context) { var rule = new ModelClientValidationRule(); rule.ErrorMessage = ErrorMessage; rule.ValidationType = "minage"; rule.ValidationParameters["min"] = _minAge; yield return rule; } } 

OK, nous avons donc implémenté GetClientValidationRules sur les deux atsortingbuts. Tout ce qui rest à faire est d’écrire les adaptateurs discrets correspondants.

Cela devrait être fait dans un fichier javascript séparé, bien sûr. Par exemple, il pourrait s’agir de sortingppleddlAdapters.js :

 (function ($) { $.fn.getDateFromTrippleDdls = function () { var year = this.find('select:nth(0)').val(); var month = this.find('select:nth(1)').val(); var day = this.find('select:nth(2)').val(); if (year == '' || month == '' || day == '') { return NaN; } var y = parseInt(year, 10); var m = parseInt(month, 10); var d = parseInt(day, 10); var date = new Date(y, m - 1, d); var isValidDate = date.getFullYear() == y && date.getMonth() + 1 == m && date.getDate() == d; if (isValidDate) { return date; } return NaN; }; $.validator.unobtrusive.adapters.add('sortingppleddldate', [], function (options) { options.rules['sortingppleddldate'] = options.params; if (options.message) { options.messages['sortingppleddldate'] = options.message; } }); $.validator.addMethod('sortingppleddldate', function (value, element, params) { var parent = $(element).closest('.sortingppleddldatetime'); var date = parent.getDateFromTrippleDdls(); console.log(date); return !isNaN(date); }, ''); $.validator.unobtrusive.adapters.add('minage', ['min'], function (options) { options.rules['minage'] = options.params; if (options.message) { options.messages['minage'] = options.message; } }); $.validator.addMethod('minage', function (value, element, params) { var parent = $(element).closest('.sortingppleddldatetime'); var birthDate = parent.getDateFromTrippleDdls(); if (isNaN(birthDate)) { return false; } var today = new Date(); var age = today.getFullYear() - birthDate.getFullYear(); var m = today.getMonth() - birthDate.getMonth(); if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) { age--; } return age >= parseInt(params.min, 10); }, ''); })(jQuery); 

Enfin, nous incluons les 3 scripts nécessaires sur la page pour permettre la validation discrète côté client:

    

J’ai essayé la solution de Darin Dimitrov, mais elle posait quelques problèmes mineurs.

L’un des problèmes était des conflits avec le validateur de date Javascript par défaut de MVC 4 – parfois, il entrait en jeu même pour des dates valides et des utilisateurs confus du site Web. J’ai inventé une solution qui peut être trouvée ici: Comment supprimer les validateurs par défaut côté client?

Le deuxième problème était que cette solution générait les mêmes atsortingbuts d’identifiant pour les trois listes déroulantes, ce qui n’est pas positif: les identifiants devraient être uniques par page HTML. Voici comment je l’ai corrigé:

 
@Html.DropDownList("", years, "Year:", new { id = @ViewData.TemplateInfo.HtmlFieldPrefix + "_y" }) @Html.DropDownList("", months, "Month:", new { id = @ViewData.TemplateInfo.HtmlFieldPrefix + "_m" }) @Html.DropDownList("", days, "Day:", new { id = @ViewData.TemplateInfo.HtmlFieldPrefix + "_d" })

Le dernier problème était que ces listes déroulantes jetaient une exception lorsque je tentais de leur prédéfinir une valeur à partir de Controller. Voici comment je l’ai corrigé:

 var result = ViewData.ModelState[ViewData.TemplateInfo.HtmlFieldPrefix]; if (result != null && result.Value != null) { var values = result.Value.RawValue as ssortingng[]; years = new SelectList(years, "Value", "Text", values[0]); months = new SelectList(months, "Value", "Text", values[1]); days = new SelectList(days, "Value", "Text", values[2]); result.Value = null; } else { var currentValue = ViewData.Model; if (currentValue != null) { years = new SelectList(years, "Value", "Text", currentValue.Year); months = new SelectList(months, "Value", "Text", currentValue.Month.ToSsortingng("00")); days = new SelectList(days, "Value", "Text", currentValue.Day.ToSsortingng("00")); } } 

Et la dernière amélioration – les noms de mois sous forme de texte:

 var months = Enumerable.Range(1, 12).Select(x => new SelectListItem { Value = x.ToSsortingng("00"), Text = System.Threading.Thread.CurrentThread.CurrentUICulture.DateTimeFormat.GetMonthName(x) }); 

Depuis le début, je voudrais dire que ce que j’écris ici est testé dans MVC 4.

J’ai essayé différentes solutions pour implémenter le sélecteur de date personnalisé basé sur 3 listes déroulantes. Tout s’est bien passé, mais comme quelqu’un l’a mentionné plus tôt dans ce message en guise de réponse, il y a eu des moments où le validateur de date standard est entré en action avec le message standard (cette chose m’a vraiment rendu fou).

Afin de résoudre ce problème et de ne pas désactiver le validateur de date standard pour de bon, j’ai trouvé la solution suivante:

a) dans l’atsortingbut de modèle de date personnalisé, utilisez la version suivante de GetClientValidationRules:

 public IEnumerable GetClientValidationRules(ModelMetadata metadata, ControllerContext context) { ModelClientValidationRule rule = new ModelClientValidationRule(); rule.ErrorMessage = this.ErrorMessageSsortingng; rule.ValidationType = "extendeddate"; rule.ValidationParameters.Add("isrequired", metadata.IsRequired.ToSsortingng().ToLower()); rule.ValidationParameters.Add("disablestandardvalidation", true.ToSsortingng().ToLower()); yield return rule; } 

Remarque: la ligne la plus importante est le dernier paramètre de validation ajouté – cela sera expliqué un peu plus tard. Pour l’instant, veuillez garder à l’esprit que cela sera transformé en un atsortingbut HTML appelé «data-val-extendeddate-disablestandardvalidation».

b) dans un fichier js externe (peu importe où – n’est qu’un exemple, mais préférable après avoir chargé toutes les bibliothèques externes), écrivez le bloc suivant:

 $(document).ready(function () { var currentCulture = $("meta[name='accept-language']").prop("content"); // Set Globalize to the current culture driven by the meta tag (if any) if (currentCulture) { Globalize.culture(currentCulture); } $.validator.methods.date = function (value, element) { var isDateValidationDisabled = $(element).data("val-extendeddate-disablestandardvalidation"); if (typeof isDateValidationDisabled != "undefined") { return true; } var val = Globalize.parseDate(value); return this.optional(element) || (val); }; $.validator.methods.number = function (value, element) { var val = Globalize.parseFloat(value); return this.optional(element) || ($.isNumeric(val)); }; }); 

Remarque: Dans ce bloc de code, je charge également le plug-in de globalisation jquery pour d’autres cultures possibles de mon application.

La partie la plus intéressante du dernier bloc de code est que si je trouve cet atsortingbut de données sur le contrôle validé, je passe la validation standard et je retourne true.

En bout de ligne et pourquoi je fais cela – lorsque vous avez un contrôle complexe qui doit être validé dans son ensemble, le validateur standard ne fonctionnera pas car il essaiera d’extraire la date de chaque liste déroulante modifiée. Il m’a fallu 8 heures pour comprendre pourquoi le validateur standard entrait toujours en action.

Bonne chance!

PS: J’espère que vous avez compris mes commentaires – je suis toujours excité par le fait que je l’ai vraiment corrigé!

J’aimerais append à la réponse de Darin Dmitrov que, lorsque vous sélectionnez l’année, le mois, puis le jour, la bordure rouge pour la validation de l’année et du mois rest active. Nous nous attendons à ce que les deux autres composants soient également synchronisés lorsqu’une date valide est entrée. J’ai donc modifié le JavaScript comme indiqué ci-dessous. (Deux fonctions, removeChildValidationErrors et addChildValidationErrors, sont ajoutées et appelées en fonction du résultat de la validation de la date.)

 (function ($) { $.fn.getDateFromTrippleDdls = function () { var year = this.find('select:nth(0)').val(); var month = this.find('select:nth(1)').val(); var day = this.find('select:nth(2)').val(); if (year == '' || month == '' || day == '') { return NaN; } var y = parseInt(year, 10); var m = parseInt(month, 10); var d = parseInt(day, 10); var date = new Date(y, m - 1, d); var isValidDate = date.getFullYear() == y && date.getMonth() + 1 == m && date.getDate() == d; if (isValidDate) { return date; } return NaN; }; $.fn.removeChildValidationErrors = function () { var year = this.find('select:nth(0)'); var month = this.find('select:nth(1)'); var day = this.find('select:nth(2)'); $(year).removeClass("input-validation-error"); $(month).removeClass("input-validation-error"); $(day).removeClass("input-validation-error"); }; $.fn.addChildValidationErrors = function () { var year = this.find('select:nth(0)'); var month = this.find('select:nth(1)'); var day = this.find('select:nth(2)'); $(year).addClass("input-validation-error"); $(month).addClass("input-validation-error"); $(day).addClass("input-validation-error"); }; $.validator.unobtrusive.adapters.add('sortingppleddldate', [], function (options) { options.rules['sortingppleddldate'] = options.params; if (options.message) { options.messages['sortingppleddldate'] = options.message; } }); $.validator.addMethod('sortingppleddldate', function (value, element, params) { var parent = $(element).closest('.sortingppleddldatetime'); var date = parent.getDateFromTrippleDdls(); if (!isNaN(date)) { parent.removeChildValidationErrors(); } else { parent.addChildValidationErrors(); } return !isNaN(date); }, ''); })(jQuery); function removeDefaultDateValidators(selector, validatorToRemove) { $('form').each(function () { var settings = $(this).validate().settings; $(selector, this).each(function () { // rules and messages seem to be keyed by element name, not id var elmName = $(this).attr('name'); delete settings.rules[elmName][validatorToRemove]; delete settings.messages[elmName][validatorToRemove]; }); }); } $(function () { removeDefaultDateValidators('select[data-val-sortingppleddldate]', 'date'); });