validation - ASP.NET MVC - Preserving invalid DateTime selection with 3 dropdown lists -


i'm still new asp.net , mvc , despite days of googling , experimenting, i'm drawing blank on best way solve problem.

i wrote birthdayattribute want work similar emailaddressattribute. birthday attribute sets ui hint birthday datetime rendered using editor template has 3 dropdown lists. attribute can used set additional meta data tells year dropdown how many years should display.

i know use jquery's date picker, in case of birthday find 3 dropdowns more usable.

@model datetime @using system; @using system.web.mvc; @{     uint16 numberofvisibleyears = 100;     if (viewdata.modelmetadata.additionalvalues.containskey("numberofvisibleyears"))     {         numberofvisibleyears = convert.touint16(viewdata.modelmetadata.additionalvalues["numberofvisibleyears"]);     }     var = datetime.now;     var years = enumerable.range(0, numberofvisibleyears).select(x => new selectlistitem { value = (now.year - x).tostring(), text = (now.year - x).tostring() });     var months = enumerable.range(1, 12).select(x => new selectlistitem{ text = new datetime( now.year, x, 1).tostring("mmmm"), value = x.tostring() });     var days = enumerable.range(1, 31).select(x => new selectlistitem { value = x.tostring("00"), text = x.tostring() }); }  @html.dropdownlist("year", years, "<year>") / @html.dropdownlist("month", months, "<month>") / @html.dropdownlist("day", days, "<day>") 

i have modelbinder rebuild date afterwards. i've removed content of helper functions brevity, works great point. normal, valid dates, work fine creating or editing members.

public class dateselector_dropdownlistbinder : defaultmodelbinder {     public override object bindmodel(controllercontext controllercontext, modelbindingcontext bindingcontext)     {         if (controllercontext == null)             throw new argumentnullexception("controllercontext");         if (bindingcontext == null)             throw new argumentnullexception("bindingcontext");          if (isdropdownlistbound(bindingcontext))         {             int year    = getdata(bindingcontext, "year");             int month   = getdata(bindingcontext, "month");             int day     = getdata(bindingcontext, "day");              datetime result;             if (!datetime.tryparse(string.format("{0}/{1}/{2}", year, month, day), out result))             {                 //todo: more useful???                 bindingcontext.modelstate.addmodelerror("", string.format("not valid date."));             }              return result;         }         else         {             return base.bindmodel(controllercontext, bindingcontext);         }      }      private int getdata(modelbindingcontext bindingcontext, string propertyname)     {         // parse int using correct value provider     }      private bool isdropdownlistbound(modelbindingcontext bindingcontext)     {         //check model meta data ui hint above editor template     } } 

now i'm looking @ it, should using nullable datetime, that's neither here nor there.

the problem i'm having basic validation of invalid dates such february 30th, or september 31st. validation works great, invalid dates aren't ever saved , persisted when form reloaded.

what i'd remember invalid date of february 30th , redisplay validation message instead of resetting dropdowns default value. other fields, email address (decorated emailaddressattribute) preserve invalid entries fine out of box.

at moment trying server side validation working. honest, haven't started thinking client side validation yet.

i know there lots javascript , ajax make problem moot point, still rather have proper server side validation in place fall on.

i managed solve problem, wanted share solution.

disclaimer: although used great .net 2.0 in day, i'm updating skills latest versions of c#, asp.net, mvc, , entity framework. if there better ways i've done below please i'm open feedback.

todo:

  • implement client side validation invalid dates such february 30th. client side validation [required] attribute built in.

  • add support cultures date shows in desired format

the solution came me when realized problem having datetime not allow constructed invalid date such february 30th. throws exception. if date wouldn't construct, knew of no way pass invalid data through binder viewmodel.

to solve problem, had away datetime in view model , replace own custom date class. solution below provide functioning server side validation in event javascript disabled. in event of validation error invalid selections persist after validation message displayed allowing user fix mistake.

it should easy enough map view-ish date class datetime in date model.

date.cs

public class date {     public date() : this( system.datetime.minvalue ) {}     public date(datetime date)     {         year = date.year;         month = date.month;         day = date.day;     }      [required]     public int year  { get; set; }      [required, range(1, 12)]     public int month { get; set; }      [required, range(1, 31)]     public int day   { get; set; }      public datetime? datetime     {                 {             datetime date;             if (!system.datetime.tryparseexact(string.format("{0}/{1}/{2}", year, month, day), "yyyy/m/d", cultureinfo.invariantculture, datetimestyles.none, out date))                 return null;             else                 return date;         }     } } 

this basic date class can construct datetime. class has properties year, month, , day datetime getter can try retrieve datetime class assuming have valid date. otherwise returns null.

when built in defaultmodelbinder mapping form date object, take care of required , range validation you. however, need new validationatribute make sure invalid dates such february 30th aren't allowed.

datevalidationattribute.cs

[attributeusage(attributetargets.property | attributetargets.field, allowmultiple = false, inherited = true)] public sealed class datevalidationattribute : validationattribute {     public datevalidationattribute(string classkey, string resourcekey) :         base(httpcontext.getglobalresourceobject(classkey, resourcekey).tostring()) { }      public override bool isvalid(object value)     {         bool result = false;         if (value == null)             throw new argumentnullexception("value");          date tovalidate = value date;          if (tovalidate == null)             throw new argumentexception("value invalid or unexpected type");          //datetime returns null when date cannot constructed         if (tovalidate.datetime != null)         {             result = (tovalidate.datetime != datetime.minvalue) && (tovalidate.datetime != datetime.maxvalue);         }          return result;     } } 

this validationattribute can put on date fields , properties. if pass in resource file class , resource key search corresponding resource file in "app_globalresources" folder error message.

inside isvalid method, once we're sure we're validating date check it's datetime property see if it's not null confirm it's valid. throw in check datetime.minvalue , maxvalue measure.

so that's really. date class, managed away custom modelbinder. solution relies on defaultmodelbinder, means of validation works right out of box. apparently checks new datevalidationattribute, super excited about. stressed forever thinking might have muck validators in custom binder. feels lot cleaner.

here complete code partial view i'm using.

dateselector_dropdownlist.cshtml

@model date @{     uint16 numberofvisibleyears = 100;     if (viewdata.modelmetadata.additionalvalues.containskey("numberofvisibleyears"))     {         numberofvisibleyears = convert.touint16(viewdata.modelmetadata.additionalvalues["numberofvisibleyears"]);     }     var = datetime.now;     var years = enumerable.range(0, numberofvisibleyears).select(x => new selectlistitem { value = (now.year - x).tostring(), text = (now.year - x).tostring() });     var months = enumerable.range(1, 12).select(x => new selectlistitem { text = new datetime(now.year, x, 1).tostring("mmmm"), value = x.tostring() });     var days = enumerable.range(1, 31).select(x => new selectlistitem { value = x.tostring(), text = x.tostring() }); }  @html.dropdownlist("year", years, "<year>") / @html.dropdownlist("month", months, "<month>") / @html.dropdownlist("day", days, "<day>") 

i'll include attribute use sets template hint , number of visible years show.

[attributeusage(attributetargets.property | attributetargets.field, allowmultiple = false)] public sealed class dateselector_dropdownlistattribute : datatypeattribute, imetadataaware {     public dateselector_dropdownlistattribute() : base(datatype.date) { }      public void onmetadatacreated(modelmetadata metadata)     {         metadata.additionalvalues.add("numberofvisibleyears", numberofvisibleyears);         metadata.templatehint = templatehint;     }      public string templatehint { get; set; }     public int numberofvisibleyears { get; set; } } 

i think solution turned out lot cleaner expected to. solves of problems in exact way hoping to. wish somehow able keep datetime, way figure out how maintain invalid selection using server side code.

are there improvements make?


Comments

Popular posts from this blog

Why does Ruby on Rails generate add a blank line to the end of a file? -

keyboard - Smiles and long press feature in Android -

node.js - Bad Request - node js ajax post -